diff --git a/.ai/instructions.md b/.ai/instructions.md new file mode 100644 index 0000000000..6504c7370d --- /dev/null +++ b/.ai/instructions.md @@ -0,0 +1,222 @@ +# ESPHome AI Collaboration Guide + +This document provides essential context for AI models interacting with this project. Adhering to these guidelines will ensure consistency and maintain code quality. + +## 1. Project Overview & Purpose + +* **Primary Goal:** ESPHome is a system to configure microcontrollers (like ESP32, ESP8266, RP2040, and LibreTiny-based chips) using simple yet powerful YAML configuration files. It generates C++ firmware that can be compiled and flashed to these devices, allowing users to control them remotely through home automation systems. +* **Business Domain:** Internet of Things (IoT), Home Automation. + +## 2. Core Technologies & Stack + +* **Languages:** Python (>=3.10), 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. +* **Key Libraries/Dependencies:** + * **Python:** `voluptuous` (for configuration validation), `PyYAML` (for parsing configuration files), `paho-mqtt` (for MQTT communication), `tornado` (for the web server), `aioesphomeapi` (for the native API). + * **C++:** `ArduinoJson` (for JSON serialization/deserialization), `AsyncMqttClient-esphome` (for MQTT), `ESPAsyncWebServer` (for the web server). +* **Package Manager(s):** `pip` (for Python dependencies), `platformio` (for C++/PlatformIO dependencies). +* **Communication Protocols:** Protobuf (for native API), MQTT, HTTP. + +## 3. Architectural Patterns + +* **Overall Architecture:** The project follows a code-generation architecture. The Python code parses user-defined YAML configuration files and generates C++ source code. This C++ code is then compiled and flashed to the target microcontroller using PlatformIO. + +* **Directory Structure Philosophy:** + * `/esphome`: Contains the core Python source code for the ESPHome application. + * `/esphome/components`: Contains the individual components that can be used in ESPHome configurations. Each component is a self-contained unit with its own C++ and Python code. + * `/tests`: Contains all unit and integration tests for the Python code. + * `/docker`: Contains Docker-related files for building and running ESPHome in a container. + * `/script`: Contains helper scripts for development and maintenance. + +* **Core Architectural Components:** + 1. **Configuration System** (`esphome/config*.py`): Handles YAML parsing and validation using Voluptuous, schema definitions, and multi-platform configurations. + 2. **Code Generation** (`esphome/codegen.py`, `esphome/cpp_generator.py`): Manages Python to C++ code generation, template processing, and build flag management. + 3. **Component System** (`esphome/components/`): Contains modular hardware and software components with platform-specific implementations and dependency management. + 4. **Core Framework** (`esphome/core/`): Manages the application lifecycle, hardware abstraction, and component registration. + 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. + 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. + +## 4. Coding Conventions & Style Guide + +* **Formatting:** + * **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`. + * **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`. + +* **Naming Conventions:** + * **Python:** Follows PEP 8. Use clear, descriptive names following snake_case. + * **C++:** Follows the Google C++ Style Guide. + +* **Component Structure:** + * **Standard Files:** + ``` + components/[component_name]/ + ├── __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 + ├── __init__.py # Platform-specific configuration + ├── [platform].h # Platform C++ header + └── [platform].cpp # Platform C++ implementation + ``` + + * **Component Metadata:** + - `DEPENDENCIES`: List of required components + - `AUTO_LOAD`: Components to automatically load + - `CONFLICTS_WITH`: Incompatible components + - `CODEOWNERS`: GitHub usernames responsible for maintenance + - `MULTI_CONF`: Whether multiple instances are allowed + +* **Code Generation & Common Patterns:** + * **Configuration Schema Pattern:** + ```python + import esphome.codegen as cg + import esphome.config_validation as cv + from esphome.const import CONF_KEY, CONF_ID + + CONF_PARAM = "param" # A constant that does not yet exist in esphome/const.py + + my_component_ns = cg.esphome_ns.namespace("my_component") + MyComponent = my_component_ns.class_("MyComponent", cg.Component) + + CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(MyComponent), + cv.Required(CONF_KEY): cv.string, + cv.Optional(CONF_PARAM, default=42): cv.int_, + }).extend(cv.COMPONENT_SCHEMA) + + async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_key(config[CONF_KEY])) + cg.add(var.set_param(config[CONF_PARAM])) + ``` + + * **C++ Class Pattern:** + ```cpp + namespace esphome { + namespace my_component { + + class MyComponent : public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + + void set_key(const std::string &key) { this->key_ = key; } + void set_param(int param) { this->param_ = param; } + + protected: + std::string key_; + int param_{0}; + }; + + } // namespace my_component + } // namespace esphome + ``` + + * **Common Component Examples:** + - **Sensor:** + ```python + from esphome.components import sensor + CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend(cv.polling_component_schema("60s")) + async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + ``` + + - **Binary Sensor:** + ```python + from esphome.components import binary_sensor + CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({ ... }) + async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + ``` + + - **Switch:** + ```python + from esphome.components import switch + CONFIG_SCHEMA = switch.switch_schema().extend({ ... }) + async def to_code(config): + var = await switch.new_switch(config) + ``` + +* **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`. + * **Schema Extensions:** + ```python + CONFIG_SCHEMA = cv.Schema({ ... }) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(i2c.i2c_device_schema(0x48)) + .extend(spi.spi_device_schema(cs_pin_required=True)) + ``` + +## 5. Key Files & Entrypoints + +* **Main Entrypoint(s):** `esphome/__main__.py` is the main entrypoint for the ESPHome command-line interface. +* **Configuration:** + * `pyproject.toml`: Defines the Python project metadata and dependencies. + * `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers. + * `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting. +* **CI/CD Pipeline:** Defined in `.github/workflows`. + +## 6. Development & Testing Workflow + +* **Local Development Environment:** Use the provided Docker container or create a Python virtual environment and install dependencies from `requirements_dev.txt`. +* **Running Commands:** Use the `script/run-in-env.py` script to execute commands within the project's virtual environment. For example, to run the linter: `python3 script/run-in-env.py pre-commit run`. +* **Testing:** + * **Python:** Run unit tests with `pytest`. + * **C++:** Use `clang-tidy` for static analysis. + * **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows: + ``` + tests/ + ├── test_build_components/ # Base test configurations + └── components/[component]/ # Component-specific tests + ``` + Run them using `script/test_build_components`. Use `-c ` to test specific components and `-t ` for specific platforms. +* **Debugging and Troubleshooting:** + * **Debug Tools:** + - `esphome config .yaml` to validate configuration. + - `esphome compile .yaml` to compile without uploading. + - Check the Dashboard for real-time logs. + - Use component-specific debug logging. + * **Common Issues:** + - **Import Errors**: Check component dependencies and `PYTHONPATH`. + - **Validation Errors**: Review configuration schema definitions. + - **Build Errors**: Check platform compatibility and library versions. + - **Runtime Errors**: Review generated C++ code and component logic. + +## 7. Specific Instructions for AI Collaboration + +* **Contribution Workflow (Pull Request Process):** + 1. **Fork & Branch:** Create a new branch in your fork. + 2. **Make Changes:** Adhere to all coding conventions and patterns. + 3. **Test:** Create component tests for all supported platforms and run the full test suite locally. + 4. **Lint:** Run `pre-commit` to ensure code is compliant. + 5. **Commit:** Commit your changes. There is no strict format for commit messages. + 6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made with the PULL_REQUEST_TEMPLATE.md template filled out correctly. + +* **Documentation Contributions:** + * Documentation is hosted in the separate `esphome/esphome-docs` repository. + * The contribution workflow is the same as for the codebase. + +* **Best Practices:** + * **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. + +* **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. + +* **Dependencies & Build System Integration:** + * **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. diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 18be8d78a9..50a7fa9709 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -07f621354fe1350ba51953c80273cd44a04aa44f15cc30bd7b8fe2a641427b7a +0c2acbc16bfb7d63571dbe7042f94f683be25e4ca8a0f158a960a94adac4b931 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5703d39be1..28437e6302 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -26,6 +26,7 @@ - [ ] RP2040 - [ ] BK72xx - [ ] RTL87xx +- [ ] nRF52840 ## Example entry for `config.yaml`: diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 120000 index 0000000000..a4b2fa310c --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +../.ai/instructions.md \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cf507bbaa6..528e69c478 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,9 @@ updates: # Hypotehsis is only used for testing and is updated quite often - dependency-name: hypothesis - package-ecosystem: github-actions + labels: + - "dependencies" + - "github-actions" directory: "/" schedule: interval: daily @@ -20,11 +23,17 @@ updates: - "docker/login-action" - "docker/setup-buildx-action" - package-ecosystem: github-actions + labels: + - "dependencies" + - "github-actions" directory: "/.github/actions/build-image" schedule: interval: daily open-pull-requests-limit: 10 - package-ecosystem: github-actions + labels: + - "dependencies" + - "github-actions" directory: "/.github/actions/restore-python" schedule: interval: daily diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml new file mode 100644 index 0000000000..c3e1c641ce --- /dev/null +++ b/.github/workflows/auto-label-pr.yml @@ -0,0 +1,449 @@ +name: Auto Label PR + +on: + # Runs only on pull_request_target due to having access to a App token. + # This means PRs from forks will not be able to alter this workflow to get the tokens + pull_request_target: + types: [labeled, opened, reopened, synchronize, edited] + +permissions: + pull-requests: write + contents: read + +env: + TARGET_PLATFORMS: | + esp32 + esp8266 + rp2040 + libretiny + bk72xx + rtl87xx + ln882x + nrf52 + host + PLATFORM_COMPONENTS: | + alarm_control_panel + audio_adc + audio_dac + binary_sensor + button + canbus + climate + cover + datetime + display + event + fan + light + lock + media_player + microphone + number + one_wire + ota + output + packet_transport + select + sensor + speaker + stepper + switch + text + text_sensor + time + touchscreen + update + valve + SMALL_PR_THRESHOLD: 30 + MAX_LABELS: 15 + +jobs: + label: + runs-on: ubuntu-latest + if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + + - name: Get changes + id: changes + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Get PR number + pr_number="${{ github.event.pull_request.number }}" + + # Get list of changed files using gh CLI + files=$(gh pr diff $pr_number --name-only) + echo "files<> $GITHUB_OUTPUT + echo "$files" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Get file stats (additions + deletions) using gh CLI + stats=$(gh pr view $pr_number --json files --jq '.files | map(.additions + .deletions) | add') + echo "total_changes=${stats:-0}" >> $GITHUB_OUTPUT + + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@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 + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const fs = require('fs'); + + const { owner, repo } = context.repo; + const pr_number = context.issue.number; + + // Get current labels + const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: pr_number + }); + const currentLabels = currentLabelsData.map(label => label.name); + + // Define managed labels that this workflow controls + const managedLabels = currentLabels.filter(label => + label.startsWith('component: ') || + [ + 'new-component', + 'new-platform', + 'new-target-platform', + 'merging-to-release', + 'merging-to-beta', + 'core', + 'small-pr', + 'dashboard', + 'github-actions', + 'by-code-owner', + 'has-tests', + 'needs-tests', + 'needs-docs', + 'too-big', + 'labeller-recheck' + ].includes(label) + ); + + console.log('Current labels:', currentLabels.join(', ')); + console.log('Managed labels:', managedLabels.join(', ')); + + // Get changed files + const changedFiles = `${{ steps.changes.outputs.files }}`.split('\n').filter(f => f.length > 0); + const totalChanges = parseInt('${{ steps.changes.outputs.total_changes }}') || 0; + + console.log('Changed files:', changedFiles.length); + console.log('Total changes:', totalChanges); + + const labels = new Set(); + + // Get environment variables + const targetPlatforms = `${{ env.TARGET_PLATFORMS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim()); + const platformComponents = `${{ env.PLATFORM_COMPONENTS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim()); + const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); + const maxLabels = parseInt('${{ env.MAX_LABELS }}'); + + // Strategy: Merge to release or beta branch + const baseRef = context.payload.pull_request.base.ref; + if (baseRef !== 'dev') { + if (baseRef === 'release') { + labels.add('merging-to-release'); + } else if (baseRef === 'beta') { + labels.add('merging-to-beta'); + } + + // When targeting non-dev branches, only use merge warning labels + const finalLabels = Array.from(labels); + console.log('Computed labels (merge branch only):', finalLabels.join(', ')); + + // Add new labels + if (finalLabels.length > 0) { + console.log(`Adding labels: ${finalLabels.join(', ')}`); + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pr_number, + labels: finalLabels + }); + } + + // Remove old managed labels that are no longer needed + const labelsToRemove = managedLabels.filter(label => + !finalLabels.includes(label) + ); + + for (const label of labelsToRemove) { + console.log(`Removing label: ${label}`); + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: pr_number, + name: label + }); + } catch (error) { + console.log(`Failed to remove label ${label}:`, error.message); + } + } + + return; // Exit early, don't process other strategies + } + + // Strategy: Component and Platform labeling + const componentRegex = /^esphome\/components\/([^\/]+)\//; + const targetPlatformRegex = new RegExp(`^esphome\/components\/(${targetPlatforms.join('|')})/`); + + for (const file of changedFiles) { + // Check for component changes + const componentMatch = file.match(componentRegex); + if (componentMatch) { + const component = componentMatch[1]; + labels.add(`component: ${component}`); + } + + // Check for target platform changes + const platformMatch = file.match(targetPlatformRegex); + if (platformMatch) { + const targetPlatform = platformMatch[1]; + labels.add(`platform: ${targetPlatform}`); + } + } + + // Get PR files for new component/platform detection + const { data: prFiles } = await github.rest.pulls.listFiles({ + owner, + repo, + pull_number: pr_number + }); + + const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); + + // Strategy: New Component detection + for (const file of addedFiles) { + // Check for new component files: esphome/components/{component}/__init__.py + const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/); + if (componentMatch) { + try { + // Read the content directly from the filesystem since we have it checked out + const content = fs.readFileSync(file, 'utf8'); + + // Strategy: New Target Platform detection + if (content.includes('IS_TARGET_PLATFORM = True')) { + labels.add('new-target-platform'); + } + labels.add('new-component'); + } catch (error) { + console.log(`Failed to read content of ${file}:`, error.message); + // Fallback: assume it's a new component if we can't read the content + labels.add('new-component'); + } + } + } + + // Strategy: New Platform detection + for (const file of addedFiles) { + // Check for new platform files: esphome/components/{component}/{platform}.py + const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/); + if (platformFileMatch) { + const [, component, platform] = platformFileMatch; + if (platformComponents.includes(platform)) { + labels.add('new-platform'); + } + } + + // Check for new platform files: esphome/components/{component}/{platform}/__init__.py + const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/); + if (platformDirMatch) { + const [, component, platform] = platformDirMatch; + if (platformComponents.includes(platform)) { + labels.add('new-platform'); + } + } + } + + const coreFiles = changedFiles.filter(file => + file.startsWith('esphome/core/') || + (file.startsWith('esphome/') && file.split('/').length === 2) + ); + + if (coreFiles.length > 0) { + labels.add('core'); + } + + // Strategy: Small PR detection + if (totalChanges <= smallPrThreshold) { + labels.add('small-pr'); + } + + // Strategy: Dashboard changes + const dashboardFiles = changedFiles.filter(file => + file.startsWith('esphome/dashboard/') || + file.startsWith('esphome/components/dashboard_import/') + ); + + if (dashboardFiles.length > 0) { + labels.add('dashboard'); + } + + // Strategy: GitHub Actions changes + const githubActionsFiles = changedFiles.filter(file => + file.startsWith('.github/workflows/') + ); + + if (githubActionsFiles.length > 0) { + labels.add('github-actions'); + } + + // Strategy: Code Owner detection + try { + // Fetch CODEOWNERS file from the repository (in case it was changed in this PR) + const { data: codeownersFile } = await github.rest.repos.getContent({ + owner, + repo, + path: 'CODEOWNERS', + }); + + const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); + const prAuthor = context.payload.pull_request.user.login; + + // Parse CODEOWNERS file + const codeownersLines = codeownersContent.split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + + let isCodeOwner = false; + + // Precompile CODEOWNERS patterns into regex objects + const codeownersRegexes = codeownersLines.map(line => { + const parts = line.split(/\s+/); + const pattern = parts[0]; + const owners = parts.slice(1); + + let regex; + if (pattern.endsWith('*')) { + // Directory pattern like "esphome/components/api/*" + const dir = pattern.slice(0, -1); + regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`); + } else if (pattern.includes('*')) { + // Glob pattern + const regexPattern = pattern + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/\\*/g, '.*'); + regex = new RegExp(`^${regexPattern}$`); + } else { + // Exact match + regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); + } + + return { regex, owners }; + }); + + for (const file of changedFiles) { + for (const { regex, owners } of codeownersRegexes) { + if (regex.test(file)) { + // Check if PR author is in the owners list + if (owners.some(owner => owner === `@${prAuthor}`)) { + isCodeOwner = true; + break; + } + } + } + if (isCodeOwner) break; + } + + if (isCodeOwner) { + labels.add('by-code-owner'); + } + } catch (error) { + console.log('Failed to read or parse CODEOWNERS file:', error.message); + } + + // Strategy: Test detection + const testFiles = changedFiles.filter(file => + file.startsWith('tests/') + ); + + if (testFiles.length > 0) { + labels.add('has-tests'); + } else { + // Only check for needs-tests if this is a new component or new platform + if (labels.has('new-component') || labels.has('new-platform')) { + labels.add('needs-tests'); + } + } + + // Strategy: Documentation check for new components/platforms + if (labels.has('new-component') || labels.has('new-platform')) { + const prBody = context.payload.pull_request.body || ''; + + // Look for documentation PR links + // Patterns to match: + // - https://github.com/esphome/esphome-docs/pull/1234 + // - esphome/esphome-docs#1234 + const docsPrPatterns = [ + /https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/, + /esphome\/esphome-docs#\d+/ + ]; + + const hasDocsLink = docsPrPatterns.some(pattern => pattern.test(prBody)); + + if (!hasDocsLink) { + labels.add('needs-docs'); + } + } + + // Convert Set to Array + let finalLabels = Array.from(labels); + + console.log('Computed labels:', finalLabels.join(', ')); + + // Don't set more than max labels + if (finalLabels.length > maxLabels) { + const originalLength = finalLabels.length; + console.log(`Not setting ${originalLength} labels because out of range`); + finalLabels = ['too-big']; + + // Request changes on the PR + await github.rest.pulls.createReview({ + owner, + repo, + pull_number: pr_number, + body: `This PR is too large and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`, + event: 'REQUEST_CHANGES' + }); + } + + // Add new labels + if (finalLabels.length > 0) { + console.log(`Adding labels: ${finalLabels.join(', ')}`); + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pr_number, + labels: finalLabels + }); + } + + // Remove old managed labels that are no longer needed + const labelsToRemove = managedLabels.filter(label => + !finalLabels.includes(label) + ); + + for (const label of labelsToRemove) { + console.log(`Removing label: ${label}`); + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: pr_number, + name: label + }); + } catch (error) { + console.log(`Failed to remove label ${label}:`, error.message); + } + } diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index f76ebba8e9..d6dac66359 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -47,7 +47,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5.6.0 with: - python-version: "3.10" + python-version: "3.11" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.11.1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f63a16844..b3f290c43f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,8 @@ permissions: contents: read env: - DEFAULT_PYTHON: "3.10" - PYUPGRADE_TARGET: "--py310-plus" + DEFAULT_PYTHON: "3.11" + PYUPGRADE_TARGET: "--py311-plus" concurrency: # yamllint disable-line rule:line-length @@ -112,7 +112,6 @@ jobs: fail-fast: false matrix: python-version: - - "3.10" - "3.11" - "3.12" - "3.13" @@ -128,14 +127,10 @@ jobs: os: windows-latest - python-version: "3.12" os: windows-latest - - python-version: "3.10" - os: windows-latest - python-version: "3.13" os: macOS-latest - python-version: "3.12" os: macOS-latest - - python-version: "3.10" - os: macOS-latest runs-on: ${{ matrix.os }} needs: - common diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml new file mode 100644 index 0000000000..ddf5698211 --- /dev/null +++ b/.github/workflows/codeowner-review-request.yml @@ -0,0 +1,264 @@ +# This workflow automatically requests reviews from codeowners when: +# 1. A PR is opened, reopened, or synchronized (updated) +# 2. A PR is marked as ready for review +# +# It reads the CODEOWNERS file and matches all changed files in the PR against +# the codeowner patterns, then requests reviews from the appropriate owners +# while avoiding duplicate requests for users who have already been requested +# or have already reviewed the PR. + +name: Request Codeowner Reviews + +on: + # Needs to be pull_request_target to get write permissions + pull_request_target: + types: [opened, reopened, synchronize, ready_for_review] + +permissions: + pull-requests: write + contents: read + +jobs: + request-codeowner-reviews: + name: Run + if: ${{ !github.event.pull_request.draft }} + runs-on: ubuntu-latest + steps: + - name: Request reviews from component codeowners + uses: actions/github-script@v7.0.1 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr_number = context.payload.pull_request.number; + + console.log(`Processing PR #${pr_number} for codeowner review requests`); + + try { + // Get the list of changed files in this PR + const { data: files } = await github.rest.pulls.listFiles({ + owner, + repo, + pull_number: pr_number + }); + + const changedFiles = files.map(file => file.filename); + console.log(`Found ${changedFiles.length} changed files`); + + if (changedFiles.length === 0) { + console.log('No changed files found, skipping codeowner review requests'); + return; + } + + // Fetch CODEOWNERS file from root + const { data: codeownersFile } = await github.rest.repos.getContent({ + owner, + repo, + path: 'CODEOWNERS', + ref: context.payload.pull_request.base.sha + }); + const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); + + // Parse CODEOWNERS file to extract all patterns and their owners + const codeownersLines = codeownersContent.split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + + const codeownersPatterns = []; + + // Convert CODEOWNERS pattern to regex (robust glob handling) + function globToRegex(pattern) { + // Escape regex special characters except for glob wildcards + let regexStr = pattern + .replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars + .replace(/\*\*/g, '.*') // globstar + .replace(/\*/g, '[^/]*') // single star + .replace(/\?/g, '.'); // question mark + return new RegExp('^' + regexStr + '$'); + } + + // Helper function to create comment body + function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) { + const reviewerMentions = reviewersList.map(r => `@${r}`); + const teamMentions = teamsList.map(t => `@${owner}/${t}`); + const allMentions = [...reviewerMentions, ...teamMentions].join(', '); + + if (isSuccessful) { + return `👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! 🙏`; + } else { + return `👋 Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! 🙏\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`; + } + } + + for (const line of codeownersLines) { + const parts = line.split(/\s+/); + if (parts.length < 2) continue; + + const pattern = parts[0]; + const owners = parts.slice(1); + + // Use robust glob-to-regex conversion + const regex = globToRegex(pattern); + codeownersPatterns.push({ pattern, regex, owners }); + } + + console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`); + + // Match changed files against CODEOWNERS patterns + const matchedOwners = new Set(); + const matchedTeams = new Set(); + const fileMatches = new Map(); // Track which files matched which patterns + + for (const file of changedFiles) { + for (const { pattern, regex, owners } of codeownersPatterns) { + if (regex.test(file)) { + console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`); + + if (!fileMatches.has(file)) { + fileMatches.set(file, []); + } + fileMatches.get(file).push({ pattern, owners }); + + // Add owners to the appropriate set (remove @ prefix) + for (const owner of owners) { + const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner; + if (cleanOwner.includes('/')) { + // Team mention (org/team-name) + const teamName = cleanOwner.split('/')[1]; + matchedTeams.add(teamName); + } else { + // Individual user + matchedOwners.add(cleanOwner); + } + } + } + } + } + + if (matchedOwners.size === 0 && matchedTeams.size === 0) { + console.log('No codeowners found for any changed files'); + return; + } + + // Remove the PR author from reviewers + const prAuthor = context.payload.pull_request.user.login; + matchedOwners.delete(prAuthor); + + // Get current reviewers to avoid duplicate requests (but still mention them) + const { data: prData } = await github.rest.pulls.get({ + owner, + repo, + pull_number: pr_number + }); + + const currentReviewers = new Set(); + const currentTeams = new Set(); + + if (prData.requested_reviewers) { + prData.requested_reviewers.forEach(reviewer => { + currentReviewers.add(reviewer.login); + }); + } + + if (prData.requested_teams) { + prData.requested_teams.forEach(team => { + currentTeams.add(team.slug); + }); + } + + // Check for completed reviews to avoid re-requesting users who have already reviewed + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: pr_number + }); + + const reviewedUsers = new Set(); + reviews.forEach(review => { + reviewedUsers.add(review.user.login); + }); + + // Remove only users who have already submitted reviews (not just requested reviewers) + reviewedUsers.forEach(reviewer => { + matchedOwners.delete(reviewer); + }); + + // For teams, we'll still remove already requested teams to avoid API errors + currentTeams.forEach(team => { + matchedTeams.delete(team); + }); + + const reviewersList = Array.from(matchedOwners); + const teamsList = Array.from(matchedTeams); + + if (reviewersList.length === 0 && teamsList.length === 0) { + console.log('No eligible reviewers found (all may already be requested or reviewed)'); + return; + } + + const totalReviewers = reviewersList.length + teamsList.length; + console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`); + + // Request reviews + try { + const requestParams = { + owner, + repo, + pull_number: pr_number + }; + + // Filter out users who are already requested reviewers for the API call + const newReviewers = reviewersList.filter(reviewer => !currentReviewers.has(reviewer)); + const newTeams = teamsList.filter(team => !currentTeams.has(team)); + + if (newReviewers.length > 0) { + requestParams.reviewers = newReviewers; + } + + if (newTeams.length > 0) { + requestParams.team_reviewers = newTeams; + } + + // Only make the API call if there are new reviewers to request + if (newReviewers.length > 0 || newTeams.length > 0) { + await github.rest.pulls.requestReviewers(requestParams); + console.log(`Successfully requested reviews from ${newReviewers.length} new users and ${newTeams.length} new teams`); + } else { + console.log('All codeowners are already requested reviewers or have reviewed'); + } + + // Add a comment to the PR mentioning what happened (include all matched codeowners) + const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr_number, + body: commentBody + }); + } catch (error) { + if (error.status === 422) { + console.log('Some reviewers may already be requested or unavailable:', error.message); + + // Try to add a comment even if review request failed + const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); + + try { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr_number, + body: commentBody + }); + } catch (commentError) { + console.log('Failed to add comment:', commentError.message); + } + } else { + throw error; + } + } + + } catch (error) { + console.log('Failed to process codeowner review requests:', error.message); + console.error(error); + } diff --git a/.github/workflows/external-component-bot.yml b/.github/workflows/external-component-bot.yml new file mode 100644 index 0000000000..5f5bc703ad --- /dev/null +++ b/.github/workflows/external-component-bot.yml @@ -0,0 +1,147 @@ +name: Add External Component Comment + +on: + pull_request_target: + types: [opened, synchronize] + +permissions: + contents: read # Needed to fetch PR details + issues: write # Needed to create and update comments (PR comments are managed via the issues REST API) + pull-requests: write # also needed? + +jobs: + external-comment: + name: External component comment + runs-on: ubuntu-latest + steps: + - name: Add external component comment + uses: actions/github-script@v7.0.1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // Generate external component usage instructions + function generateExternalComponentInstructions(prNumber, componentNames, owner, repo) { + let source; + if (owner === 'esphome' && repo === 'esphome') + source = `github://pr#${prNumber}`; + else + source = `github://${owner}/${repo}@pull/${prNumber}/head`; + return `To use the changes from this PR as an external component, add the following to your ESPHome configuration YAML file: + + \`\`\`yaml + external_components: + - source: ${source} + components: [${componentNames.join(', ')}] + refresh: 1h + \`\`\``; + } + + // Generate repo clone instructions + function generateRepoInstructions(prNumber, owner, repo, branch) { + return `To use the changes in this PR: + + \`\`\`bash + # Clone the repository: + git clone https://github.com/${owner}/${repo} + cd ${repo} + + # Checkout the PR branch: + git fetch origin pull/${prNumber}/head:${branch} + git checkout ${branch} + + # Install the development version: + script/setup + + # Activate the development version: + source venv/bin/activate + \`\`\` + + Now you can run \`esphome\` as usual to test the changes in this PR. + `; + } + + async function createComment(octokit, owner, repo, prNumber, esphomeChanges, componentChanges) { + const commentMarker = ""; + let commentBody; + if (esphomeChanges.length === 1) { + commentBody = generateExternalComponentInstructions(prNumber, componentChanges, owner, repo); + } else { + commentBody = generateRepoInstructions(prNumber, owner, repo, context.payload.pull_request.head.ref); + } + commentBody += `\n\n---\n(Added by the PR bot)\n\n${commentMarker}`; + + // Check for existing bot comment + const comments = await github.rest.issues.listComments({ + owner: owner, + repo: repo, + issue_number: prNumber, + }); + + const botComment = comments.data.find(comment => + comment.body.includes(commentMarker) + ); + + if (botComment && botComment.body === commentBody) { + // No changes in the comment, do nothing + return; + } + + if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: owner, + repo: repo, + comment_id: botComment.id, + body: commentBody, + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: prNumber, + body: commentBody, + }); + } + } + + async function getEsphomeAndComponentChanges(github, owner, repo, prNumber) { + const changedFiles = await github.rest.pulls.listFiles({ + owner: owner, + repo: repo, + pull_number: prNumber, + }); + + const esphomeChanges = changedFiles.data + .filter(file => file.filename !== "esphome/core/defines.h" && file.filename.startsWith('esphome/')) + .map(file => { + const match = file.filename.match(/esphome\/([^/]+)/); + return match ? match[1] : null; + }) + .filter(it => it !== null); + + if (esphomeChanges.length === 0) { + return {esphomeChanges: [], componentChanges: []}; + } + + const uniqueEsphomeChanges = [...new Set(esphomeChanges)]; + const componentChanges = changedFiles.data + .filter(file => file.filename.startsWith('esphome/components/')) + .map(file => { + const match = file.filename.match(/esphome\/components\/([^/]+)\//); + return match ? match[1] : null; + }) + .filter(it => it !== null); + + return {esphomeChanges: uniqueEsphomeChanges, componentChanges: [...new Set(componentChanges)]}; + } + + // Start of main code. + + const prNumber = context.payload.pull_request.number; + const {owner, repo} = context.repo; + + const {esphomeChanges, componentChanges} = await getEsphomeAndComponentChanges(github, owner, repo, prNumber); + if (componentChanges.length !== 0) { + await createComment(github, owner, repo, prNumber, esphomeChanges, componentChanges); + } diff --git a/.github/workflows/issue-codeowner-notify.yml b/.github/workflows/issue-codeowner-notify.yml new file mode 100644 index 0000000000..3ff9c58510 --- /dev/null +++ b/.github/workflows/issue-codeowner-notify.yml @@ -0,0 +1,119 @@ +# This workflow automatically notifies codeowners when an issue is labeled with component labels. +# It reads the CODEOWNERS file to find the maintainers for the labeled components +# and posts a comment mentioning them to ensure they're aware of the issue. + +name: Notify Issue Codeowners + +on: + issues: + types: [labeled] + +permissions: + issues: write + contents: read + +jobs: + notify-codeowners: + name: Run + if: ${{ startsWith(github.event.label.name, format('component{0} ', ':')) }} + runs-on: ubuntu-latest + steps: + - name: Notify codeowners for component issues + uses: actions/github-script@v7.0.1 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const issue_number = context.payload.issue.number; + const labelName = context.payload.label.name; + + console.log(`Processing issue #${issue_number} with label: ${labelName}`); + + // Extract component name from label + const componentName = labelName.replace('component: ', ''); + console.log(`Component: ${componentName}`); + + try { + // Fetch CODEOWNERS file from root + const { data: codeownersFile } = await github.rest.repos.getContent({ + owner, + repo, + path: 'CODEOWNERS' + }); + const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); + + // Parse CODEOWNERS file to extract component mappings + const codeownersLines = codeownersContent.split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + + let componentOwners = null; + + for (const line of codeownersLines) { + const parts = line.split(/\s+/); + if (parts.length < 2) continue; + + const pattern = parts[0]; + const owners = parts.slice(1); + + // Look for component patterns: esphome/components/{component}/* + const componentMatch = pattern.match(/^esphome\/components\/([^\/]+)\/\*$/); + if (componentMatch && componentMatch[1] === componentName) { + componentOwners = owners; + break; + } + } + + if (!componentOwners) { + console.log(`No codeowners found for component: ${componentName}`); + return; + } + + console.log(`Found codeowners for '${componentName}': ${componentOwners.join(', ')}`); + + // Separate users and teams + const userOwners = []; + const teamOwners = []; + + for (const owner of componentOwners) { + const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner; + if (cleanOwner.includes('/')) { + // Team mention (org/team-name) + teamOwners.push(`@${cleanOwner}`); + } else { + // Individual user + userOwners.push(`@${cleanOwner}`); + } + } + + // Remove issue author from mentions to avoid self-notification + const issueAuthor = context.payload.issue.user.login; + const filteredUserOwners = userOwners.filter(mention => + mention !== `@${issueAuthor}` + ); + + const allMentions = [...filteredUserOwners, ...teamOwners]; + + if (allMentions.length === 0) { + console.log('No codeowners to notify (issue author is the only codeowner)'); + return; + } + + // Create comment body + const mentionString = allMentions.join(', '); + const commentBody = `👋 Hey ${mentionString}!\n\nThis issue has been labeled with \`component: ${componentName}\` and you've been identified as a codeowner of this component. Please take a look when you have a chance!\n\nThanks for maintaining this component! 🙏`; + + // Post comment + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issue_number, + body: commentBody + }); + + console.log(`Successfully notified codeowners: ${mentionString}`); + + } catch (error) { + console.log('Failed to process codeowner notifications:', error.message); + console.error(error); + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b4518b27b5..44919a6270 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,7 +96,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5.6.0 with: - python-version: "3.10" + python-version: "3.11" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.11.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 118253861d..b5b45f27aa 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.3 + rev: v0.12.4 hooks: # Run the linter. - id: ruff @@ -40,7 +40,7 @@ repos: rev: v3.20.0 hooks: - id: pyupgrade - args: [--py310-plus] + args: [--py311-plus] - repo: https://github.com/adrienverge/yamllint.git rev: v1.37.1 hooks: diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000000..49e811ff05 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +.ai/instructions.md \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index 2975080ba9..257f927fd9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -324,6 +324,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw esphome/components/nfc/* @jesserockz @kbx81 esphome/components/noblex/* @AGalfra esphome/components/npi19/* @bakerkj +esphome/components/nrf52/* @tomaszduda23 esphome/components/number/* @esphome/core esphome/components/one_wire/* @ssieb esphome/components/online_image/* @clydebarrow @guillempages @@ -378,6 +379,7 @@ esphome/components/rp2040_pwm/* @jesserockz esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtttl/* @glmnet +esphome/components/runtime_stats/* @bdraco esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/scd4x/* @martgras @sjtrny esphome/components/script/* @esphome/core @@ -535,5 +537,6 @@ esphome/components/xiaomi_xmwsdj04mmc/* @medusalix esphome/components/xl9535/* @mreditor97 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xxtea/* @clydebarrow +esphome/components/zephyr/* @tomaszduda23 esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zio_ultrasonic/* @kahrendt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7be7bdac2c..303b548310 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ project and be sure to join us on [Discord](https://discord.gg/KhAMKrd). **See also:** -[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues) +[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/esphome/issues) -- [Feature requests](https://github.com/orgs/esphome/discussions) --- diff --git a/GEMINI.md b/GEMINI.md new file mode 120000 index 0000000000..49e811ff05 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +.ai/instructions.md \ No newline at end of file diff --git a/README.md b/README.md index 4f527870b8..0439b1bc06 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ --- -[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues) +[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/esphome/issues) -- [Feature requests](https://github.com/orgs/esphome/discussions) --- diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 10b7df8638..efe3b190af 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -5,6 +5,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C2, VARIANT_ESP32C3, + VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2, VARIANT_ESP32S2, @@ -51,82 +52,93 @@ SAMPLING_MODES = { "max": sampling_mode.MAX, } -adc1_channel_t = cg.global_ns.enum("adc1_channel_t") -adc2_channel_t = cg.global_ns.enum("adc2_channel_t") +adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True) + +adc_channel_t = cg.global_ns.enum("adc_channel_t", is_class=True) # pin to adc1 channel mapping # https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h VARIANT_ESP32: { - 36: adc1_channel_t.ADC1_CHANNEL_0, - 37: adc1_channel_t.ADC1_CHANNEL_1, - 38: adc1_channel_t.ADC1_CHANNEL_2, - 39: adc1_channel_t.ADC1_CHANNEL_3, - 32: adc1_channel_t.ADC1_CHANNEL_4, - 33: adc1_channel_t.ADC1_CHANNEL_5, - 34: adc1_channel_t.ADC1_CHANNEL_6, - 35: adc1_channel_t.ADC1_CHANNEL_7, + 36: adc_channel_t.ADC_CHANNEL_0, + 37: adc_channel_t.ADC_CHANNEL_1, + 38: adc_channel_t.ADC_CHANNEL_2, + 39: adc_channel_t.ADC_CHANNEL_3, + 32: adc_channel_t.ADC_CHANNEL_4, + 33: adc_channel_t.ADC_CHANNEL_5, + 34: adc_channel_t.ADC_CHANNEL_6, + 35: adc_channel_t.ADC_CHANNEL_7, }, # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h VARIANT_ESP32C2: { - 0: adc1_channel_t.ADC1_CHANNEL_0, - 1: adc1_channel_t.ADC1_CHANNEL_1, - 2: adc1_channel_t.ADC1_CHANNEL_2, - 3: adc1_channel_t.ADC1_CHANNEL_3, - 4: adc1_channel_t.ADC1_CHANNEL_4, + 0: adc_channel_t.ADC_CHANNEL_0, + 1: adc_channel_t.ADC_CHANNEL_1, + 2: adc_channel_t.ADC_CHANNEL_2, + 3: adc_channel_t.ADC_CHANNEL_3, + 4: adc_channel_t.ADC_CHANNEL_4, }, # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h VARIANT_ESP32C3: { - 0: adc1_channel_t.ADC1_CHANNEL_0, - 1: adc1_channel_t.ADC1_CHANNEL_1, - 2: adc1_channel_t.ADC1_CHANNEL_2, - 3: adc1_channel_t.ADC1_CHANNEL_3, - 4: adc1_channel_t.ADC1_CHANNEL_4, + 0: adc_channel_t.ADC_CHANNEL_0, + 1: adc_channel_t.ADC_CHANNEL_1, + 2: adc_channel_t.ADC_CHANNEL_2, + 3: adc_channel_t.ADC_CHANNEL_3, + 4: adc_channel_t.ADC_CHANNEL_4, + }, + # ESP32-C5 ADC1 pin mapping - based on official ESP-IDF documentation + # https://docs.espressif.com/projects/esp-idf/en/latest/esp32c5/api-reference/peripherals/gpio.html + VARIANT_ESP32C5: { + 1: adc_channel_t.ADC_CHANNEL_0, + 2: adc_channel_t.ADC_CHANNEL_1, + 3: adc_channel_t.ADC_CHANNEL_2, + 4: adc_channel_t.ADC_CHANNEL_3, + 5: adc_channel_t.ADC_CHANNEL_4, + 6: adc_channel_t.ADC_CHANNEL_5, }, # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h VARIANT_ESP32C6: { - 0: adc1_channel_t.ADC1_CHANNEL_0, - 1: adc1_channel_t.ADC1_CHANNEL_1, - 2: adc1_channel_t.ADC1_CHANNEL_2, - 3: adc1_channel_t.ADC1_CHANNEL_3, - 4: adc1_channel_t.ADC1_CHANNEL_4, - 5: adc1_channel_t.ADC1_CHANNEL_5, - 6: adc1_channel_t.ADC1_CHANNEL_6, + 0: adc_channel_t.ADC_CHANNEL_0, + 1: adc_channel_t.ADC_CHANNEL_1, + 2: adc_channel_t.ADC_CHANNEL_2, + 3: adc_channel_t.ADC_CHANNEL_3, + 4: adc_channel_t.ADC_CHANNEL_4, + 5: adc_channel_t.ADC_CHANNEL_5, + 6: adc_channel_t.ADC_CHANNEL_6, }, # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h VARIANT_ESP32H2: { - 1: adc1_channel_t.ADC1_CHANNEL_0, - 2: adc1_channel_t.ADC1_CHANNEL_1, - 3: adc1_channel_t.ADC1_CHANNEL_2, - 4: adc1_channel_t.ADC1_CHANNEL_3, - 5: adc1_channel_t.ADC1_CHANNEL_4, + 1: adc_channel_t.ADC_CHANNEL_0, + 2: adc_channel_t.ADC_CHANNEL_1, + 3: adc_channel_t.ADC_CHANNEL_2, + 4: adc_channel_t.ADC_CHANNEL_3, + 5: adc_channel_t.ADC_CHANNEL_4, }, # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h VARIANT_ESP32S2: { - 1: adc1_channel_t.ADC1_CHANNEL_0, - 2: adc1_channel_t.ADC1_CHANNEL_1, - 3: adc1_channel_t.ADC1_CHANNEL_2, - 4: adc1_channel_t.ADC1_CHANNEL_3, - 5: adc1_channel_t.ADC1_CHANNEL_4, - 6: adc1_channel_t.ADC1_CHANNEL_5, - 7: adc1_channel_t.ADC1_CHANNEL_6, - 8: adc1_channel_t.ADC1_CHANNEL_7, - 9: adc1_channel_t.ADC1_CHANNEL_8, - 10: adc1_channel_t.ADC1_CHANNEL_9, + 1: adc_channel_t.ADC_CHANNEL_0, + 2: adc_channel_t.ADC_CHANNEL_1, + 3: adc_channel_t.ADC_CHANNEL_2, + 4: adc_channel_t.ADC_CHANNEL_3, + 5: adc_channel_t.ADC_CHANNEL_4, + 6: adc_channel_t.ADC_CHANNEL_5, + 7: adc_channel_t.ADC_CHANNEL_6, + 8: adc_channel_t.ADC_CHANNEL_7, + 9: adc_channel_t.ADC_CHANNEL_8, + 10: adc_channel_t.ADC_CHANNEL_9, }, # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h VARIANT_ESP32S3: { - 1: adc1_channel_t.ADC1_CHANNEL_0, - 2: adc1_channel_t.ADC1_CHANNEL_1, - 3: adc1_channel_t.ADC1_CHANNEL_2, - 4: adc1_channel_t.ADC1_CHANNEL_3, - 5: adc1_channel_t.ADC1_CHANNEL_4, - 6: adc1_channel_t.ADC1_CHANNEL_5, - 7: adc1_channel_t.ADC1_CHANNEL_6, - 8: adc1_channel_t.ADC1_CHANNEL_7, - 9: adc1_channel_t.ADC1_CHANNEL_8, - 10: adc1_channel_t.ADC1_CHANNEL_9, + 1: adc_channel_t.ADC_CHANNEL_0, + 2: adc_channel_t.ADC_CHANNEL_1, + 3: adc_channel_t.ADC_CHANNEL_2, + 4: adc_channel_t.ADC_CHANNEL_3, + 5: adc_channel_t.ADC_CHANNEL_4, + 6: adc_channel_t.ADC_CHANNEL_5, + 7: adc_channel_t.ADC_CHANNEL_6, + 8: adc_channel_t.ADC_CHANNEL_7, + 9: adc_channel_t.ADC_CHANNEL_8, + 10: adc_channel_t.ADC_CHANNEL_9, }, } @@ -135,54 +147,56 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h VARIANT_ESP32: { - 4: adc2_channel_t.ADC2_CHANNEL_0, - 0: adc2_channel_t.ADC2_CHANNEL_1, - 2: adc2_channel_t.ADC2_CHANNEL_2, - 15: adc2_channel_t.ADC2_CHANNEL_3, - 13: adc2_channel_t.ADC2_CHANNEL_4, - 12: adc2_channel_t.ADC2_CHANNEL_5, - 14: adc2_channel_t.ADC2_CHANNEL_6, - 27: adc2_channel_t.ADC2_CHANNEL_7, - 25: adc2_channel_t.ADC2_CHANNEL_8, - 26: adc2_channel_t.ADC2_CHANNEL_9, + 4: adc_channel_t.ADC_CHANNEL_0, + 0: adc_channel_t.ADC_CHANNEL_1, + 2: adc_channel_t.ADC_CHANNEL_2, + 15: adc_channel_t.ADC_CHANNEL_3, + 13: adc_channel_t.ADC_CHANNEL_4, + 12: adc_channel_t.ADC_CHANNEL_5, + 14: adc_channel_t.ADC_CHANNEL_6, + 27: adc_channel_t.ADC_CHANNEL_7, + 25: adc_channel_t.ADC_CHANNEL_8, + 26: adc_channel_t.ADC_CHANNEL_9, }, # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h VARIANT_ESP32C2: { - 5: adc2_channel_t.ADC2_CHANNEL_0, + 5: adc_channel_t.ADC_CHANNEL_0, }, # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h VARIANT_ESP32C3: { - 5: adc2_channel_t.ADC2_CHANNEL_0, + 5: adc_channel_t.ADC_CHANNEL_0, }, + # ESP32-C5 has no ADC2 channels + VARIANT_ESP32C5: {}, # no ADC2 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h VARIANT_ESP32C6: {}, # no ADC2 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h VARIANT_ESP32H2: {}, # no ADC2 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h VARIANT_ESP32S2: { - 11: adc2_channel_t.ADC2_CHANNEL_0, - 12: adc2_channel_t.ADC2_CHANNEL_1, - 13: adc2_channel_t.ADC2_CHANNEL_2, - 14: adc2_channel_t.ADC2_CHANNEL_3, - 15: adc2_channel_t.ADC2_CHANNEL_4, - 16: adc2_channel_t.ADC2_CHANNEL_5, - 17: adc2_channel_t.ADC2_CHANNEL_6, - 18: adc2_channel_t.ADC2_CHANNEL_7, - 19: adc2_channel_t.ADC2_CHANNEL_8, - 20: adc2_channel_t.ADC2_CHANNEL_9, + 11: adc_channel_t.ADC_CHANNEL_0, + 12: adc_channel_t.ADC_CHANNEL_1, + 13: adc_channel_t.ADC_CHANNEL_2, + 14: adc_channel_t.ADC_CHANNEL_3, + 15: adc_channel_t.ADC_CHANNEL_4, + 16: adc_channel_t.ADC_CHANNEL_5, + 17: adc_channel_t.ADC_CHANNEL_6, + 18: adc_channel_t.ADC_CHANNEL_7, + 19: adc_channel_t.ADC_CHANNEL_8, + 20: adc_channel_t.ADC_CHANNEL_9, }, # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h VARIANT_ESP32S3: { - 11: adc2_channel_t.ADC2_CHANNEL_0, - 12: adc2_channel_t.ADC2_CHANNEL_1, - 13: adc2_channel_t.ADC2_CHANNEL_2, - 14: adc2_channel_t.ADC2_CHANNEL_3, - 15: adc2_channel_t.ADC2_CHANNEL_4, - 16: adc2_channel_t.ADC2_CHANNEL_5, - 17: adc2_channel_t.ADC2_CHANNEL_6, - 18: adc2_channel_t.ADC2_CHANNEL_7, - 19: adc2_channel_t.ADC2_CHANNEL_8, - 20: adc2_channel_t.ADC2_CHANNEL_9, + 11: adc_channel_t.ADC_CHANNEL_0, + 12: adc_channel_t.ADC_CHANNEL_1, + 13: adc_channel_t.ADC_CHANNEL_2, + 14: adc_channel_t.ADC_CHANNEL_3, + 15: adc_channel_t.ADC_CHANNEL_4, + 16: adc_channel_t.ADC_CHANNEL_5, + 17: adc_channel_t.ADC_CHANNEL_6, + 18: adc_channel_t.ADC_CHANNEL_7, + 19: adc_channel_t.ADC_CHANNEL_8, + 20: adc_channel_t.ADC_CHANNEL_9, }, } diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 28dfd2262c..a60272a1f7 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -3,12 +3,15 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/voltage_sampler/voltage_sampler.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #ifdef USE_ESP32 -#include -#include "driver/adc.h" -#endif // USE_ESP32 +#include "esp_adc/adc_cali.h" +#include "esp_adc/adc_cali_scheme.h" +#include "esp_adc/adc_oneshot.h" +#include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX +#endif // USE_ESP32 namespace esphome { namespace adc { @@ -49,36 +52,72 @@ class Aggregator { class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { public: -#ifdef USE_ESP32 - /// Set the attenuation for this pin. Only available on the ESP32. - void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } - void set_channel1(adc1_channel_t channel) { - this->channel1_ = channel; - this->channel2_ = ADC2_CHANNEL_MAX; - } - void set_channel2(adc2_channel_t channel) { - this->channel2_ = channel; - this->channel1_ = ADC1_CHANNEL_MAX; - } - void set_autorange(bool autorange) { this->autorange_ = autorange; } -#endif // USE_ESP32 - - /// Update ADC values + /// Update the sensor's state by reading the current ADC value. + /// This method is called periodically based on the update interval. void update() override; - /// Setup ADC + + /// Set up the ADC sensor by initializing hardware and calibration parameters. + /// This method is called once during device initialization. void setup() override; + + /// Output the configuration details of the ADC sensor for debugging purposes. + /// This method is called during the ESPHome setup process to log the configuration. void dump_config() override; - /// `HARDWARE_LATE` setup priority + + /// Return the setup priority for this component. + /// Components with higher priority are initialized earlier during setup. + /// @return A float representing the setup priority. float get_setup_priority() const override; + + /// Set the GPIO pin to be used by the ADC sensor. + /// @param pin Pointer to an InternalGPIOPin representing the ADC input pin. void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } + + /// Enable or disable the output of raw ADC values (unprocessed data). + /// @param output_raw Boolean indicating whether to output raw ADC values (true) or processed values (false). void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; } + + /// Set the number of samples to be taken for ADC readings to improve accuracy. + /// A higher sample count reduces noise but increases the reading time. + /// @param sample_count The number of samples (e.g., 1, 4, 8). void set_sample_count(uint8_t sample_count); + + /// Set the sampling mode for how multiple ADC samples are combined into a single measurement. + /// + /// When multiple samples are taken (controlled by set_sample_count), they can be combined + /// in one of three ways: + /// - SamplingMode::AVG: Compute the average (default) + /// - SamplingMode::MIN: Use the lowest sample value + /// - SamplingMode::MAX: Use the highest sample value + /// @param sampling_mode The desired sampling mode to use for aggregating ADC samples. void set_sampling_mode(SamplingMode sampling_mode); + + /// Perform a single ADC sampling operation and return the measured value. + /// This function handles raw readings, calibration, and averaging as needed. + /// @return The sampled value as a float. float sample() override; -#ifdef USE_ESP8266 - std::string unique_id() override; -#endif // USE_ESP8266 +#ifdef USE_ESP32 + /// Set the ADC attenuation level to adjust the input voltage range. + /// This determines how the ADC interprets input voltages, allowing for greater precision + /// or the ability to measure higher voltages depending on the chosen attenuation level. + /// @param attenuation The desired ADC attenuation level (e.g., ADC_ATTEN_DB_0, ADC_ATTEN_DB_11). + void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } + + /// Configure the ADC to use a specific channel on a specific ADC unit. + /// This sets the channel for single-shot or continuous ADC measurements. + /// @param unit The ADC unit to use (ADC_UNIT_1 or ADC_UNIT_2). + /// @param channel The ADC channel to configure, such as ADC_CHANNEL_0, ADC_CHANNEL_3, etc. + void set_channel(adc_unit_t unit, adc_channel_t channel) { + this->adc_unit_ = unit; + this->channel_ = channel; + } + + /// Set whether autoranging should be enabled for the ADC. + /// Autoranging automatically adjusts the attenuation level to handle a wide range of input voltages. + /// @param autorange Boolean indicating whether to enable autoranging. + void set_autorange(bool autorange) { this->autorange_ = autorange; } +#endif // USE_ESP32 #ifdef USE_RP2040 void set_is_temperature() { this->is_temperature_ = true; } @@ -90,17 +129,28 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage InternalGPIOPin *pin_; SamplingMode sampling_mode_{SamplingMode::AVG}; +#ifdef USE_ESP32 + float sample_autorange_(); + float sample_fixed_attenuation_(); + bool autorange_{false}; + adc_oneshot_unit_handle_t adc_handle_{nullptr}; + adc_cali_handle_t calibration_handle_{nullptr}; + adc_atten_t attenuation_{ADC_ATTEN_DB_0}; + adc_channel_t channel_; + adc_unit_t adc_unit_; + struct SetupFlags { + uint8_t init_complete : 1; + uint8_t config_complete : 1; + uint8_t handle_init_complete : 1; + uint8_t calibration_complete : 1; + uint8_t reserved : 4; + } setup_flags_{}; + static adc_oneshot_unit_handle_t shared_adc_handles[2]; +#endif // USE_ESP32 + #ifdef USE_RP2040 bool is_temperature_{false}; #endif // USE_RP2040 - -#ifdef USE_ESP32 - adc_atten_t attenuation_{ADC_ATTEN_DB_0}; - adc1_channel_t channel1_{ADC1_CHANNEL_MAX}; - adc2_channel_t channel2_{ADC2_CHANNEL_MAX}; - bool autorange_{false}; - esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {}; -#endif // USE_ESP32 }; } // namespace adc diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index ed1f3329ab..f3503b49c9 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -8,145 +8,315 @@ namespace adc { static const char *const TAG = "adc.esp32"; -static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast(ADC_WIDTH_MAX - 1); +adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr}; -#ifndef SOC_ADC_RTC_MAX_BITWIDTH -#if USE_ESP32_VARIANT_ESP32S2 -static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; -#else -static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; -#endif // USE_ESP32_VARIANT_ESP32S2 -#endif // SOC_ADC_RTC_MAX_BITWIDTH - -static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; -static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; - -void ADCSensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); - - if (this->channel1_ != ADC1_CHANNEL_MAX) { - adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); - if (!this->autorange_) { - adc1_config_channel_atten(this->channel1_, this->attenuation_); - } - } else if (this->channel2_ != ADC2_CHANNEL_MAX) { - if (!this->autorange_) { - adc2_config_channel_atten(this->channel2_, this->attenuation_); - } - } - - for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) { - auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2; - auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS, - 1100, // default vref - &this->cal_characteristics_[i]); - switch (cal_value) { - case ESP_ADC_CAL_VAL_EFUSE_VREF: - ESP_LOGV(TAG, "Using eFuse Vref for calibration"); - break; - case ESP_ADC_CAL_VAL_EFUSE_TP: - ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration"); - break; - case ESP_ADC_CAL_VAL_DEFAULT_VREF: - default: - break; - } +const LogString *attenuation_to_str(adc_atten_t attenuation) { + switch (attenuation) { + case ADC_ATTEN_DB_0: + return LOG_STR("0 dB"); + case ADC_ATTEN_DB_2_5: + return LOG_STR("2.5 dB"); + case ADC_ATTEN_DB_6: + return LOG_STR("6 dB"); + case ADC_ATTEN_DB_12_COMPAT: + return LOG_STR("12 dB"); + default: + return LOG_STR("Unknown Attenuation"); } } -void ADCSensor::dump_config() { - static const char *const ATTEN_AUTO_STR = "auto"; - static const char *const ATTEN_0DB_STR = "0 db"; - static const char *const ATTEN_2_5DB_STR = "2.5 db"; - static const char *const ATTEN_6DB_STR = "6 db"; - static const char *const ATTEN_12DB_STR = "12 db"; - const char *atten_str = ATTEN_AUTO_STR; +const LogString *adc_unit_to_str(adc_unit_t unit) { + switch (unit) { + case ADC_UNIT_1: + return LOG_STR("ADC1"); + case ADC_UNIT_2: + return LOG_STR("ADC2"); + default: + return LOG_STR("Unknown ADC Unit"); + } +} - LOG_SENSOR("", "ADC Sensor", this); - LOG_PIN(" Pin: ", this->pin_); - - if (!this->autorange_) { - switch (this->attenuation_) { - case ADC_ATTEN_DB_0: - atten_str = ATTEN_0DB_STR; - break; - case ADC_ATTEN_DB_2_5: - atten_str = ATTEN_2_5DB_STR; - break; - case ADC_ATTEN_DB_6: - atten_str = ATTEN_6DB_STR; - break; - case ADC_ATTEN_DB_12_COMPAT: - atten_str = ATTEN_12DB_STR; - break; - default: // This is to satisfy the unused ADC_ATTEN_MAX - break; +void ADCSensor::setup() { + ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); + // Check if another sensor already initialized this ADC unit + if (ADCSensor::shared_adc_handles[this->adc_unit_] == nullptr) { + adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize + init_config.unit_id = this->adc_unit_; + init_config.ulp_mode = ADC_ULP_MODE_DISABLE; +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2 + init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; +#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || + // USE_ESP32_VARIANT_ESP32H2 + esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); + this->mark_failed(); + return; } } + this->adc_handle_ = ADCSensor::shared_adc_handles[this->adc_unit_]; + this->setup_flags_.handle_init_complete = true; + + adc_oneshot_chan_cfg_t config = { + .atten = this->attenuation_, + .bitwidth = ADC_BITWIDTH_DEFAULT, + }; + esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Error configuring channel: %d", err); + this->mark_failed(); + return; + } + this->setup_flags_.config_complete = true; + + // Initialize ADC calibration + if (this->calibration_handle_ == nullptr) { + adc_cali_handle_t handle = nullptr; + esp_err_t err; + +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 + // RISC-V variants and S3 use curve fitting calibration + adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + cali_config.chan = this->channel_; +#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + cali_config.unit_id = this->adc_unit_; + cali_config.atten = this->attenuation_; + cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; + + err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); + if (err == ESP_OK) { + this->calibration_handle_ = handle; + this->setup_flags_.calibration_complete = true; + ESP_LOGV(TAG, "Using curve fitting calibration"); + } else { + ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err); + this->setup_flags_.calibration_complete = false; + } +#else // Other ESP32 variants use line fitting calibration + adc_cali_line_fitting_config_t cali_config = { + .unit_id = this->adc_unit_, + .atten = this->attenuation_, + .bitwidth = ADC_BITWIDTH_DEFAULT, +#if !defined(USE_ESP32_VARIANT_ESP32S2) + .default_vref = 1100, // Default reference voltage in mV +#endif // !defined(USE_ESP32_VARIANT_ESP32S2) + }; + err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); + if (err == ESP_OK) { + this->calibration_handle_ = handle; + this->setup_flags_.calibration_complete = true; + ESP_LOGV(TAG, "Using line fitting calibration"); + } else { + ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); + this->setup_flags_.calibration_complete = false; + } +#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2 + } + + this->setup_flags_.init_complete = true; +} + +void ADCSensor::dump_config() { + LOG_SENSOR("", "ADC Sensor", this); + LOG_PIN(" Pin: ", this->pin_); ESP_LOGCONFIG(TAG, - " Attenuation: %s\n" - " Samples: %i\n" + " Channel: %d\n" + " Unit: %s\n" + " Attenuation: %s\n" + " Samples: %i\n" " Sampling mode: %s", - atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); + this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), + this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_, + LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); + + ESP_LOGCONFIG( + TAG, + " Setup Status:\n" + " Handle Init: %s\n" + " Config: %s\n" + " Calibration: %s\n" + " Overall Init: %s", + this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED", + this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED"); + LOG_UPDATE_INTERVAL(this); } float ADCSensor::sample() { - if (!this->autorange_) { - auto aggr = Aggregator(this->sampling_mode_); + if (this->autorange_) { + return this->sample_autorange_(); + } else { + return this->sample_fixed_attenuation_(); + } +} - for (uint8_t sample = 0; sample < this->sample_count_; sample++) { - int raw = -1; - if (this->channel1_ != ADC1_CHANNEL_MAX) { - raw = adc1_get_raw(this->channel1_); - } else if (this->channel2_ != ADC2_CHANNEL_MAX) { - adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw); - } - if (raw == -1) { - return NAN; - } +float ADCSensor::sample_fixed_attenuation_() { + auto aggr = Aggregator(this->sampling_mode_); - aggr.add_sample(raw); + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { + int raw; + esp_err_t err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); + + if (err != ESP_OK) { + ESP_LOGW(TAG, "ADC read failed with error %d", err); + continue; } - if (this->output_raw_) { - return aggr.aggregate(); + + if (raw == -1) { + ESP_LOGW(TAG, "Invalid ADC reading"); + continue; } - uint32_t mv = - esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]); - return mv / 1000.0f; + + aggr.add_sample(raw); } - int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; + uint32_t final_value = aggr.aggregate(); - if (this->channel1_ != ADC1_CHANNEL_MAX) { - adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT); - raw12 = adc1_get_raw(this->channel1_); - if (raw12 < ADC_MAX) { - adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6); - raw6 = adc1_get_raw(this->channel1_); - if (raw6 < ADC_MAX) { - adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5); - raw2 = adc1_get_raw(this->channel1_); - if (raw2 < ADC_MAX) { - adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0); - raw0 = adc1_get_raw(this->channel1_); - } + if (this->output_raw_) { + return final_value; + } + + if (this->calibration_handle_ != nullptr) { + int voltage_mv; + esp_err_t err = adc_cali_raw_to_voltage(this->calibration_handle_, final_value, &voltage_mv); + if (err == ESP_OK) { + return voltage_mv / 1000.0f; + } else { + ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); + if (this->calibration_handle_ != nullptr) { +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 + adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); +#else // Other ESP32 variants use line fitting calibration + adc_cali_delete_scheme_line_fitting(this->calibration_handle_); +#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2 + this->calibration_handle_ = nullptr; } } - } else if (this->channel2_ != ADC2_CHANNEL_MAX) { - adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_12_COMPAT); - adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw12); - if (raw12 < ADC_MAX) { - adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_6); - adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6); - if (raw6 < ADC_MAX) { - adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5); - adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); - if (raw2 < ADC_MAX) { - adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0); - adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); - } + } + + return final_value * 3.3f / 4095.0f; +} + +float ADCSensor::sample_autorange_() { + // Auto-range mode + auto read_atten = [this](adc_atten_t atten) -> std::pair { + // First reconfigure the attenuation for this reading + adc_oneshot_chan_cfg_t config = { + .atten = atten, + .bitwidth = ADC_BITWIDTH_DEFAULT, + }; + + esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config); + + if (err != ESP_OK) { + ESP_LOGW(TAG, "Error configuring ADC channel for autorange: %d", err); + return {-1, 0.0f}; + } + + // Need to recalibrate for the new attenuation + if (this->calibration_handle_ != nullptr) { + // Delete old calibration handle +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 + adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); +#else + adc_cali_delete_scheme_line_fitting(this->calibration_handle_); +#endif + this->calibration_handle_ = nullptr; + } + + // Create new calibration handle for this attenuation + adc_cali_handle_t handle = nullptr; + +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 + adc_cali_curve_fitting_config_t cali_config = {}; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + cali_config.chan = this->channel_; +#endif + cali_config.unit_id = this->adc_unit_; + cali_config.atten = atten; + cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; + + err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); +#else + adc_cali_line_fitting_config_t cali_config = { + .unit_id = this->adc_unit_, + .atten = atten, + .bitwidth = ADC_BITWIDTH_DEFAULT, +#if !defined(USE_ESP32_VARIANT_ESP32S2) + .default_vref = 1100, +#endif + }; + err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); +#endif + + int raw; + err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); + + if (err != ESP_OK) { + ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); + if (handle != nullptr) { +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 + adc_cali_delete_scheme_curve_fitting(handle); +#else + adc_cali_delete_scheme_line_fitting(handle); +#endif + } + return {-1, 0.0f}; + } + + float voltage = 0.0f; + if (handle != nullptr) { + int voltage_mv; + err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv); + if (err == ESP_OK) { + voltage = voltage_mv / 1000.0f; + } else { + voltage = raw * 3.3f / 4095.0f; + } + // Clean up calibration handle +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 + adc_cali_delete_scheme_curve_fitting(handle); +#else + adc_cali_delete_scheme_line_fitting(handle); +#endif + } else { + voltage = raw * 3.3f / 4095.0f; + } + + return {raw, voltage}; + }; + + auto [raw12, mv12] = read_atten(ADC_ATTEN_DB_12); + if (raw12 == -1) { + ESP_LOGE(TAG, "Failed to read ADC in autorange mode"); + return NAN; + } + + int raw6 = 4095, raw2 = 4095, raw0 = 4095; + float mv6 = 0, mv2 = 0, mv0 = 0; + + if (raw12 < 4095) { + auto [raw6_val, mv6_val] = read_atten(ADC_ATTEN_DB_6); + raw6 = raw6_val; + mv6 = mv6_val; + + if (raw6 < 4095 && raw6 != -1) { + auto [raw2_val, mv2_val] = read_atten(ADC_ATTEN_DB_2_5); + raw2 = raw2_val; + mv2 = mv2_val; + + if (raw2 < 4095 && raw2 != -1) { + auto [raw0_val, mv0_val] = read_atten(ADC_ATTEN_DB_0); + raw0 = raw0_val; + mv0 = mv0_val; } } } @@ -155,19 +325,19 @@ float ADCSensor::sample() { return NAN; } - uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]); - uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); - uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); - uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); - - 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(ADC_MAX - raw0, ADC_HALF); + 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; - uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); - return mv_scaled / (float) (csum * 1000U); + if (csum == 0) { + ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); + return NAN; + } + + return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; } } // namespace adc diff --git a/esphome/components/adc/adc_sensor_esp8266.cpp b/esphome/components/adc/adc_sensor_esp8266.cpp index 67128fb1f3..1123d83830 100644 --- a/esphome/components/adc/adc_sensor_esp8266.cpp +++ b/esphome/components/adc/adc_sensor_esp8266.cpp @@ -56,8 +56,6 @@ float ADCSensor::sample() { return aggr.aggregate() / 1024.0f; } -std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } - } // namespace adc } // namespace esphome diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 3309bd04c5..01bbaeda15 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -10,13 +10,11 @@ from esphome.const import ( CONF_NUMBER, CONF_PIN, CONF_RAW, - CONF_WIFI, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, UNIT_VOLT, ) from esphome.core import CORE -import esphome.final_validate as fv from . import ( ATTENUATION_MODES, @@ -24,6 +22,7 @@ from . import ( ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, SAMPLING_MODES, adc_ns, + adc_unit_t, validate_adc_pin, ) @@ -57,21 +56,6 @@ def validate_config(config): return config -def final_validate_config(config): - if CORE.is_esp32: - variant = get_esp32_variant() - if ( - CONF_WIFI in fv.full_config.get() - and config[CONF_PIN][CONF_NUMBER] - in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] - ): - raise cv.Invalid( - f"{variant} doesn't support ADC on this pin when Wi-Fi is configured" - ) - - return config - - ADCSensor = adc_ns.class_( "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler ) @@ -99,8 +83,6 @@ CONFIG_SCHEMA = cv.All( validate_config, ) -FINAL_VALIDATE_SCHEMA = final_validate_config - async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) @@ -119,13 +101,13 @@ async def to_code(config): cg.add(var.set_sample_count(config[CONF_SAMPLES])) cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) - if attenuation := config.get(CONF_ATTENUATION): - if attenuation == "auto": - cg.add(var.set_autorange(cg.global_ns.true)) - else: - cg.add(var.set_attenuation(attenuation)) - if CORE.is_esp32: + if attenuation := config.get(CONF_ATTENUATION): + if attenuation == "auto": + cg.add(var.set_autorange(cg.global_ns.true)) + else: + cg.add(var.set_attenuation(attenuation)) + variant = get_esp32_variant() pin_num = config[CONF_PIN][CONF_NUMBER] if ( @@ -133,10 +115,10 @@ async def to_code(config): and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] ): chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] - cg.add(var.set_channel1(chan)) + cg.add(var.set_channel(adc_unit_t.ADC_UNIT_1, chan)) elif ( variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] ): chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] - cg.add(var.set_channel2(chan)) + cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan)) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 861b3471d7..b0ce21b1ce 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -222,37 +222,37 @@ message DeviceInfoResponse { // The model of the board. For example NodeMCU string model = 6; - bool has_deep_sleep = 7; + bool has_deep_sleep = 7 [(field_ifdef) = "USE_DEEP_SLEEP"]; // The esphome project details if set - string project_name = 8; - string project_version = 9; + string project_name = 8 [(field_ifdef) = "ESPHOME_PROJECT_NAME"]; + string project_version = 9 [(field_ifdef) = "ESPHOME_PROJECT_NAME"]; - uint32 webserver_port = 10; + uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"]; - uint32 legacy_bluetooth_proxy_version = 11; - uint32 bluetooth_proxy_feature_flags = 15; + uint32 legacy_bluetooth_proxy_version = 11 [(field_ifdef) = "USE_BLUETOOTH_PROXY"]; + uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"]; string manufacturer = 12; string friendly_name = 13; - uint32 legacy_voice_assistant_version = 14; - uint32 voice_assistant_feature_flags = 17; + uint32 legacy_voice_assistant_version = 14 [(field_ifdef) = "USE_VOICE_ASSISTANT"]; + uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"]; - string suggested_area = 16; + string suggested_area = 16 [(field_ifdef) = "USE_AREAS"]; // The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA" - string bluetooth_mac_address = 18; + string bluetooth_mac_address = 18 [(field_ifdef) = "USE_BLUETOOTH_PROXY"]; // Supports receiving and saving api encryption key - bool api_encryption_supported = 19; + bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"]; - repeated DeviceInfo devices = 20; - repeated AreaInfo areas = 21; + repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES"]; + repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS"]; // Top-level area info to phase out suggested_area - AreaInfo area = 22; + AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"]; } message ListEntitiesRequest { @@ -290,14 +290,14 @@ message ListEntitiesBinarySensorResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id string device_class = 5; bool is_status_binary_sensor = 6; bool disabled_by_default = 7; - string icon = 8; + string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 9; - uint32 device_id = 10; + uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message BinarySensorStateResponse { option (id) = 21; @@ -311,7 +311,7 @@ message BinarySensorStateResponse { // If the binary sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } // ==================== COVER ==================== @@ -324,17 +324,17 @@ message ListEntitiesCoverResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id bool assumed_state = 5; bool supports_position = 6; bool supports_tilt = 7; string device_class = 8; bool disabled_by_default = 9; - string icon = 10; + string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 11; bool supports_stop = 12; - uint32 device_id = 13; + uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } enum LegacyCoverState { @@ -361,7 +361,7 @@ message CoverStateResponse { float position = 3; float tilt = 4; CoverOperation current_operation = 5; - uint32 device_id = 6; + uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"]; } enum LegacyCoverCommand { @@ -388,7 +388,7 @@ message CoverCommandRequest { bool has_tilt = 6; float tilt = 7; bool stop = 8; - uint32 device_id = 9; + uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } // ==================== FAN ==================== @@ -401,17 +401,17 @@ message ListEntitiesFanResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id bool supports_oscillation = 5; bool supports_speed = 6; bool supports_direction = 7; int32 supported_speed_count = 8; bool disabled_by_default = 9; - string icon = 10; + string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 11; repeated string supported_preset_modes = 12; - uint32 device_id = 13; + uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } enum FanSpeed { FAN_SPEED_LOW = 0; @@ -436,7 +436,7 @@ message FanStateResponse { FanDirection direction = 5; int32 speed_level = 6; string preset_mode = 7; - uint32 device_id = 8; + uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; } message FanCommandRequest { option (id) = 31; @@ -458,7 +458,7 @@ message FanCommandRequest { int32 speed_level = 11; bool has_preset_mode = 12; string preset_mode = 13; - uint32 device_id = 14; + uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; } // ==================== LIGHT ==================== @@ -484,7 +484,7 @@ message ListEntitiesLightResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id repeated ColorMode supported_color_modes = 12; // next four supports_* are for legacy clients, newer clients should use color modes @@ -496,9 +496,9 @@ message ListEntitiesLightResponse { float max_mireds = 10; repeated string effects = 11; bool disabled_by_default = 13; - string icon = 14; + string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 15; - uint32 device_id = 16; + uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"]; } message LightStateResponse { option (id) = 24; @@ -520,7 +520,7 @@ message LightStateResponse { float cold_white = 12; float warm_white = 13; string effect = 9; - uint32 device_id = 14; + uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; } message LightCommandRequest { option (id) = 32; @@ -556,7 +556,7 @@ message LightCommandRequest { uint32 flash_length = 17; bool has_effect = 18; string effect = 19; - uint32 device_id = 28; + uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"]; } // ==================== SENSOR ==================== @@ -582,9 +582,9 @@ message ListEntitiesSensorResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; string unit_of_measurement = 6; int32 accuracy_decimals = 7; bool force_update = 8; @@ -594,7 +594,7 @@ message ListEntitiesSensorResponse { SensorLastResetType legacy_last_reset_type = 11; bool disabled_by_default = 12; EntityCategory entity_category = 13; - uint32 device_id = 14; + uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; } message SensorStateResponse { option (id) = 25; @@ -608,7 +608,7 @@ message SensorStateResponse { // If the sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } // ==================== SWITCH ==================== @@ -621,14 +621,14 @@ message ListEntitiesSwitchResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool assumed_state = 6; bool disabled_by_default = 7; EntityCategory entity_category = 8; string device_class = 9; - uint32 device_id = 10; + uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message SwitchStateResponse { option (id) = 26; @@ -639,7 +639,7 @@ message SwitchStateResponse { fixed32 key = 1; bool state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } message SwitchCommandRequest { option (id) = 33; @@ -650,7 +650,7 @@ message SwitchCommandRequest { fixed32 key = 1; bool state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== TEXT SENSOR ==================== @@ -663,13 +663,13 @@ message ListEntitiesTextSensorResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; - uint32 device_id = 9; + uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message TextSensorStateResponse { option (id) = 27; @@ -683,7 +683,7 @@ message TextSensorStateResponse { // If the text sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } // ==================== SUBSCRIBE LOGS ==================== @@ -853,11 +853,11 @@ message ListEntitiesCameraResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id bool disabled_by_default = 5; - string icon = 6; + string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 7; - uint32 device_id = 8; + uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; } message CameraImageResponse { @@ -869,7 +869,7 @@ message CameraImageResponse { fixed32 key = 1; bytes data = 2; bool done = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message CameraImageRequest { option (id) = 45; @@ -937,7 +937,7 @@ message ListEntitiesClimateResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id bool supports_current_temperature = 5; bool supports_two_point_target_temperature = 6; @@ -955,14 +955,14 @@ message ListEntitiesClimateResponse { repeated ClimatePreset supported_presets = 16; repeated string supported_custom_presets = 17; bool disabled_by_default = 18; - string icon = 19; + 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; float visual_min_humidity = 24; float visual_max_humidity = 25; - uint32 device_id = 26; + uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"]; } message ClimateStateResponse { option (id) = 47; @@ -987,7 +987,7 @@ message ClimateStateResponse { string custom_preset = 13; float current_humidity = 14; float target_humidity = 15; - uint32 device_id = 16; + uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"]; } message ClimateCommandRequest { option (id) = 48; @@ -1020,7 +1020,7 @@ message ClimateCommandRequest { string custom_preset = 21; bool has_target_humidity = 22; float target_humidity = 23; - uint32 device_id = 24; + uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"]; } // ==================== NUMBER ==================== @@ -1038,9 +1038,9 @@ message ListEntitiesNumberResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; float min_value = 6; float max_value = 7; float step = 8; @@ -1049,7 +1049,7 @@ message ListEntitiesNumberResponse { string unit_of_measurement = 11; NumberMode mode = 12; string device_class = 13; - uint32 device_id = 14; + uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; } message NumberStateResponse { option (id) = 50; @@ -1063,7 +1063,7 @@ message NumberStateResponse { // If the number does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message NumberCommandRequest { option (id) = 51; @@ -1074,7 +1074,7 @@ message NumberCommandRequest { fixed32 key = 1; float state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== SELECT ==================== @@ -1087,13 +1087,13 @@ message ListEntitiesSelectResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; repeated string options = 6; bool disabled_by_default = 7; EntityCategory entity_category = 8; - uint32 device_id = 9; + uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message SelectStateResponse { option (id) = 53; @@ -1107,7 +1107,7 @@ message SelectStateResponse { // If the select does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message SelectCommandRequest { option (id) = 54; @@ -1118,7 +1118,7 @@ message SelectCommandRequest { fixed32 key = 1; string state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== SIREN ==================== @@ -1131,15 +1131,15 @@ message ListEntitiesSirenResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; repeated string tones = 7; bool supports_duration = 8; bool supports_volume = 9; EntityCategory entity_category = 10; - uint32 device_id = 11; + uint32 device_id = 11 [(field_ifdef) = "USE_DEVICES"]; } message SirenStateResponse { option (id) = 56; @@ -1150,7 +1150,7 @@ message SirenStateResponse { fixed32 key = 1; bool state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } message SirenCommandRequest { option (id) = 57; @@ -1168,7 +1168,7 @@ message SirenCommandRequest { uint32 duration = 7; bool has_volume = 8; float volume = 9; - uint32 device_id = 10; + uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } // ==================== LOCK ==================== @@ -1194,9 +1194,9 @@ message ListEntitiesLockResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; bool assumed_state = 8; @@ -1206,7 +1206,7 @@ message ListEntitiesLockResponse { // Not yet implemented: string code_format = 11; - uint32 device_id = 12; + uint32 device_id = 12 [(field_ifdef) = "USE_DEVICES"]; } message LockStateResponse { option (id) = 59; @@ -1216,7 +1216,7 @@ message LockStateResponse { option (no_delay) = true; fixed32 key = 1; LockState state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } message LockCommandRequest { option (id) = 60; @@ -1230,7 +1230,7 @@ message LockCommandRequest { // Not yet implemented: bool has_code = 3; string code = 4; - uint32 device_id = 5; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } // ==================== BUTTON ==================== @@ -1243,13 +1243,13 @@ message ListEntitiesButtonResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; - uint32 device_id = 9; + uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message ButtonCommandRequest { option (id) = 62; @@ -1259,7 +1259,7 @@ message ButtonCommandRequest { option (base_class) = "CommandProtoMessage"; fixed32 key = 1; - uint32 device_id = 2; + uint32 device_id = 2 [(field_ifdef) = "USE_DEVICES"]; } // ==================== MEDIA PLAYER ==================== @@ -1298,9 +1298,9 @@ message ListEntitiesMediaPlayerResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; @@ -1308,7 +1308,7 @@ message ListEntitiesMediaPlayerResponse { repeated MediaPlayerSupportedFormat supported_formats = 9; - uint32 device_id = 10; + uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message MediaPlayerStateResponse { option (id) = 64; @@ -1320,7 +1320,7 @@ message MediaPlayerStateResponse { MediaPlayerState state = 2; float volume = 3; bool muted = 4; - uint32 device_id = 5; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } message MediaPlayerCommandRequest { option (id) = 65; @@ -1342,7 +1342,7 @@ message MediaPlayerCommandRequest { bool has_announcement = 8; bool announcement = 9; - uint32 device_id = 10; + uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } // ==================== BLUETOOTH ==================== @@ -1381,7 +1381,7 @@ message BluetoothLERawAdvertisement { sint32 rssi = 2; uint32 address_type = 3; - bytes data = 4; + bytes data = 4 [(fixed_array_size) = 62]; } message BluetoothLERawAdvertisementsResponse { @@ -1845,14 +1845,14 @@ message ListEntitiesAlarmControlPanelResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; - string icon = 5; + reserved 4; // Deprecated: was string unique_id + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 supported_features = 8; bool requires_code = 9; bool requires_code_to_arm = 10; - uint32 device_id = 11; + uint32 device_id = 11 [(field_ifdef) = "USE_DEVICES"]; } message AlarmControlPanelStateResponse { @@ -1863,7 +1863,7 @@ message AlarmControlPanelStateResponse { option (no_delay) = true; fixed32 key = 1; AlarmControlPanelState state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } message AlarmControlPanelCommandRequest { @@ -1875,7 +1875,7 @@ message AlarmControlPanelCommandRequest { fixed32 key = 1; AlarmControlPanelStateCommand command = 2; string code = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } // ===================== TEXT ===================== @@ -1892,8 +1892,8 @@ message ListEntitiesTextResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; - string icon = 5; + reserved 4; // Deprecated: was string unique_id + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; @@ -1901,7 +1901,7 @@ message ListEntitiesTextResponse { uint32 max_length = 9; string pattern = 10; TextMode mode = 11; - uint32 device_id = 12; + uint32 device_id = 12 [(field_ifdef) = "USE_DEVICES"]; } message TextStateResponse { option (id) = 98; @@ -1915,7 +1915,7 @@ message TextStateResponse { // If the Text does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message TextCommandRequest { option (id) = 99; @@ -1926,7 +1926,7 @@ message TextCommandRequest { fixed32 key = 1; string state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1940,12 +1940,12 @@ message ListEntitiesDateResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - uint32 device_id = 8; + uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; } message DateStateResponse { option (id) = 101; @@ -1961,7 +1961,7 @@ message DateStateResponse { uint32 year = 3; uint32 month = 4; uint32 day = 5; - uint32 device_id = 6; + uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"]; } message DateCommandRequest { option (id) = 102; @@ -1974,7 +1974,7 @@ message DateCommandRequest { uint32 year = 2; uint32 month = 3; uint32 day = 4; - uint32 device_id = 5; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } // ==================== DATETIME TIME ==================== @@ -1987,12 +1987,12 @@ message ListEntitiesTimeResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - uint32 device_id = 8; + uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; } message TimeStateResponse { option (id) = 104; @@ -2008,7 +2008,7 @@ message TimeStateResponse { uint32 hour = 3; uint32 minute = 4; uint32 second = 5; - uint32 device_id = 6; + uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"]; } message TimeCommandRequest { option (id) = 105; @@ -2021,7 +2021,7 @@ message TimeCommandRequest { uint32 hour = 2; uint32 minute = 3; uint32 second = 4; - uint32 device_id = 5; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } // ==================== EVENT ==================== @@ -2034,15 +2034,15 @@ message ListEntitiesEventResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; repeated string event_types = 9; - uint32 device_id = 10; + uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message EventResponse { option (id) = 108; @@ -2052,7 +2052,7 @@ message EventResponse { fixed32 key = 1; string event_type = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== VALVE ==================== @@ -2065,9 +2065,9 @@ message ListEntitiesValveResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; @@ -2075,7 +2075,7 @@ message ListEntitiesValveResponse { bool assumed_state = 9; bool supports_position = 10; bool supports_stop = 11; - uint32 device_id = 12; + uint32 device_id = 12 [(field_ifdef) = "USE_DEVICES"]; } enum ValveOperation { @@ -2093,7 +2093,7 @@ message ValveStateResponse { fixed32 key = 1; float position = 2; ValveOperation current_operation = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message ValveCommandRequest { @@ -2107,7 +2107,7 @@ message ValveCommandRequest { bool has_position = 2; float position = 3; bool stop = 4; - uint32 device_id = 5; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } // ==================== DATETIME DATETIME ==================== @@ -2120,12 +2120,12 @@ message ListEntitiesDateTimeResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - uint32 device_id = 8; + uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; } message DateTimeStateResponse { option (id) = 113; @@ -2139,7 +2139,7 @@ message DateTimeStateResponse { // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; fixed32 epoch_seconds = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message DateTimeCommandRequest { option (id) = 114; @@ -2150,7 +2150,7 @@ message DateTimeCommandRequest { fixed32 key = 1; fixed32 epoch_seconds = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== UPDATE ==================== @@ -2163,13 +2163,13 @@ message ListEntitiesUpdateResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; - uint32 device_id = 9; + uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message UpdateStateResponse { option (id) = 117; @@ -2188,7 +2188,7 @@ message UpdateStateResponse { string title = 8; string release_summary = 9; string release_url = 10; - uint32 device_id = 11; + uint32 device_id = 11 [(field_ifdef) = "USE_DEVICES"]; } enum UpdateCommand { UPDATE_COMMAND_NONE = 0; @@ -2204,5 +2204,5 @@ message UpdateCommandRequest { fixed32 key = 1; UpdateCommand command = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ea3268a583..2ac3303691 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -42,18 +42,37 @@ static const char *const TAG = "api.connection"; static const int CAMERA_STOP_STREAM = 5000; #endif -// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call object +#ifdef USE_DEVICES +// Helper macro for entity command handlers - gets entity by key and device_id, returns if not found, and creates call +// object +#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \ + entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \ + if ((entity_var) == nullptr) \ + return; \ + auto call = (entity_var)->make_call(); + +// Helper macro for entity command handlers that don't use make_call() - gets entity by key and device_id and returns if +// not found +#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \ + entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \ + if ((entity_var) == nullptr) \ + return; +#else // No device support, use simpler macros +// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call +// object #define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \ entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ if ((entity_var) == nullptr) \ return; \ auto call = (entity_var)->make_call(); -// Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if not found +// Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if +// not found #define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \ entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ if ((entity_var) == nullptr) \ return; +#endif // USE_DEVICES APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { @@ -86,8 +105,8 @@ void APIConnection::start() { APIError err = this->helper_->init(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(), - api_error_to_str(err), errno); + ESP_LOGW(TAG, "%s: Helper init failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), + errno); return; } this->client_info_ = helper_->getpeername(); @@ -119,7 +138,7 @@ void APIConnection::loop() { APIError err = this->helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), + ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), errno); return; } @@ -136,14 +155,8 @@ void APIConnection::loop() { break; } else if (err != APIError::OK) { on_fatal_error(); - if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); - } else if (err == APIError::CONNECTION_CLOSED) { - ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str()); - } else { - ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(), - api_error_to_str(err), errno); - } + ESP_LOGW(TAG, "%s: Reading failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), + errno); return; } else { this->last_traffic_ = now; @@ -186,9 +199,11 @@ void APIConnection::loop() { on_fatal_error(); ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); } - } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { + } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { + // Only send ping if we're not disconnecting ESP_LOGVV(TAG, "Sending keepalive PING"); - this->flags_.sent_ping = this->send_message(PingRequest()); + PingRequest req; + this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); if (!this->flags_.sent_ping) { // If we can't send the ping request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority @@ -237,7 +252,7 @@ void APIConnection::loop() { resp.entity_id = it.entity_id; resp.attribute = it.attribute.value(); resp.once = it.once; - if (this->send_message(resp)) { + if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { state_subs_at_++; } } else { @@ -246,10 +261,6 @@ void APIConnection::loop() { } } -std::string get_default_unique_id(const std::string &component_type, EntityBase *entity) { - return App.get_name() + component_type + entity->get_object_id(); -} - DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response @@ -326,8 +337,8 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn BinarySensorStateResponse resp; resp.state = binary_sensor->state; resp.missing_state = !binary_sensor->has_state(); - fill_entity_state_base(binary_sensor, resp); - return encode_message_to_buffer(resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(binary_sensor, resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, + remaining_size, is_single); } uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -336,9 +347,8 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne ListEntitiesBinarySensorResponse msg; msg.device_class = binary_sensor->get_device_class(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); - msg.unique_id = get_default_unique_id("binary_sensor", binary_sensor); - fill_entity_info_base(binary_sensor, msg); - return encode_message_to_buffer(msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, + remaining_size, is_single); } #endif @@ -358,8 +368,7 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * if (traits.get_supports_tilt()) msg.tilt = cover->tilt; msg.current_operation = static_cast(cover->current_operation); - fill_entity_state_base(cover, msg); - return encode_message_to_buffer(msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -371,9 +380,8 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); msg.device_class = cover->get_device_class(); - msg.unique_id = get_default_unique_id("cover", cover); - fill_entity_info_base(cover, msg); - return encode_message_to_buffer(msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::cover_command(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) @@ -420,8 +428,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co msg.direction = static_cast(fan->direction); if (traits.supports_preset_modes()) msg.preset_mode = fan->preset_mode; - fill_entity_state_base(fan, msg); - return encode_message_to_buffer(msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + 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, bool is_single) { @@ -434,9 +441,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supported_speed_count = traits.supported_speed_count(); for (auto const &preset : traits.supported_preset_modes()) msg.supported_preset_modes.push_back(preset); - msg.unique_id = get_default_unique_id("fan", fan); - fill_entity_info_base(fan, msg); - return encode_message_to_buffer(msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::fan_command(const FanCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) @@ -481,8 +486,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * resp.warm_white = values.get_warm_white(); if (light->supports_effects()) resp.effect = light->get_effect_name(); - fill_entity_state_base(light, resp); - return encode_message_to_buffer(resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -508,9 +512,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c msg.effects.push_back(effect->get_name()); } } - msg.unique_id = get_default_unique_id("light", light); - fill_entity_info_base(light, msg); - return encode_message_to_buffer(msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::light_command(const LightCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) @@ -557,8 +560,7 @@ uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection SensorStateResponse resp; resp.state = sensor->state; resp.missing_state = !sensor->has_state(); - fill_entity_state_base(sensor, resp); - return encode_message_to_buffer(resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -570,11 +572,8 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * msg.force_update = sensor->get_force_update(); msg.device_class = sensor->get_device_class(); msg.state_class = static_cast(sensor->get_state_class()); - msg.unique_id = sensor->unique_id(); - if (msg.unique_id.empty()) - msg.unique_id = get_default_unique_id("sensor", sensor); - fill_entity_info_base(sensor, msg); - return encode_message_to_buffer(msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } #endif @@ -589,8 +588,8 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection auto *a_switch = static_cast(entity); SwitchStateResponse resp; resp.state = a_switch->state; - fill_entity_state_base(a_switch, resp); - return encode_message_to_buffer(resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -599,9 +598,8 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection * ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); msg.device_class = a_switch->get_device_class(); - msg.unique_id = get_default_unique_id("switch", a_switch); - fill_entity_info_base(a_switch, msg); - return encode_message_to_buffer(msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) @@ -626,19 +624,16 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec TextSensorStateResponse resp; resp.state = text_sensor->state; resp.missing_state = !text_sensor->has_state(); - fill_entity_state_base(text_sensor, resp); - return encode_message_to_buffer(resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *text_sensor = static_cast(entity); ListEntitiesTextSensorResponse msg; msg.device_class = text_sensor->get_device_class(); - msg.unique_id = text_sensor->unique_id(); - if (msg.unique_id.empty()) - msg.unique_id = get_default_unique_id("text_sensor", text_sensor); - fill_entity_info_base(text_sensor, msg); - return encode_message_to_buffer(msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, + remaining_size, is_single); } #endif @@ -651,7 +646,6 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection bool is_single) { auto *climate = static_cast(entity); ClimateStateResponse resp; - fill_entity_state_base(climate, resp); auto traits = climate->get_traits(); resp.mode = static_cast(climate->mode); resp.action = static_cast(climate->action); @@ -678,7 +672,8 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection resp.current_humidity = climate->current_humidity; if (traits.get_supports_target_humidity()) resp.target_humidity = climate->target_humidity; - return encode_message_to_buffer(resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -709,9 +704,8 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supported_custom_presets.push_back(custom_preset); for (auto swing_mode : traits.get_supported_swing_modes()) msg.supported_swing_modes.push_back(static_cast(swing_mode)); - msg.unique_id = get_default_unique_id("climate", climate); - fill_entity_info_base(climate, msg); - return encode_message_to_buffer(msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) @@ -751,8 +745,7 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection NumberStateResponse resp; resp.state = number->state; resp.missing_state = !number->has_state(); - fill_entity_state_base(number, resp); - return encode_message_to_buffer(resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -765,9 +758,8 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - msg.unique_id = get_default_unique_id("number", number); - fill_entity_info_base(number, msg); - return encode_message_to_buffer(msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::number_command(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) @@ -789,16 +781,14 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c resp.year = date->year; resp.month = date->month; resp.day = date->day; - fill_entity_state_base(date, resp); - return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *date = static_cast(entity); ListEntitiesDateResponse msg; - msg.unique_id = get_default_unique_id("date", date); - fill_entity_info_base(date, msg); - return encode_message_to_buffer(msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::date_command(const DateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) @@ -820,16 +810,14 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c resp.hour = time->hour; resp.minute = time->minute; resp.second = time->second; - fill_entity_state_base(time, resp); - return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *time = static_cast(entity); ListEntitiesTimeResponse msg; - msg.unique_id = get_default_unique_id("time", time); - fill_entity_info_base(time, msg); - return encode_message_to_buffer(msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::time_command(const TimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) @@ -852,16 +840,15 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio ESPTime state = datetime->state_as_esptime(); resp.epoch_seconds = state.timestamp; } - fill_entity_state_base(datetime, resp); - return encode_message_to_buffer(resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *datetime = static_cast(entity); ListEntitiesDateTimeResponse msg; - msg.unique_id = get_default_unique_id("datetime", datetime); - fill_entity_info_base(datetime, msg); - return encode_message_to_buffer(msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) @@ -882,8 +869,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c TextStateResponse resp; resp.state = text->state; resp.missing_state = !text->has_state(); - fill_entity_state_base(text, resp); - return encode_message_to_buffer(resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -894,9 +880,8 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co msg.min_length = text->traits.get_min_length(); msg.max_length = text->traits.get_max_length(); msg.pattern = text->traits.get_pattern(); - msg.unique_id = get_default_unique_id("text", text); - fill_entity_info_base(text, msg); - return encode_message_to_buffer(msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::text_command(const TextCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) @@ -917,8 +902,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection SelectStateResponse resp; resp.state = select->state; resp.missing_state = !select->has_state(); - fill_entity_state_base(select, resp); - return encode_message_to_buffer(resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -927,9 +911,8 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection * ListEntitiesSelectResponse msg; for (const auto &option : select->traits.get_options()) msg.options.push_back(option); - msg.unique_id = get_default_unique_id("select", select); - fill_entity_info_base(select, msg); - return encode_message_to_buffer(msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::select_command(const SelectCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) @@ -944,9 +927,8 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection * auto *button = static_cast(entity); ListEntitiesButtonResponse msg; msg.device_class = button->get_device_class(); - msg.unique_id = get_default_unique_id("button", button); - fill_entity_info_base(button, msg); - return encode_message_to_buffer(msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) @@ -965,8 +947,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c auto *a_lock = static_cast(entity); LockStateResponse resp; resp.state = static_cast(a_lock->state); - fill_entity_state_base(a_lock, resp); - return encode_message_to_buffer(resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -976,9 +957,8 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co msg.assumed_state = a_lock->traits.get_assumed_state(); msg.supports_open = a_lock->traits.get_supports_open(); msg.requires_code = a_lock->traits.get_requires_code(); - msg.unique_id = get_default_unique_id("lock", a_lock); - fill_entity_info_base(a_lock, msg); - return encode_message_to_buffer(msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::lock_command(const LockCommandRequest &msg) { ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) @@ -1008,8 +988,7 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection * ValveStateResponse resp; resp.position = valve->position; resp.current_operation = static_cast(valve->current_operation); - fill_entity_state_base(valve, resp); - return encode_message_to_buffer(resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1020,9 +999,8 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - msg.unique_id = get_default_unique_id("valve", valve); - fill_entity_info_base(valve, msg); - return encode_message_to_buffer(msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::valve_command(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) @@ -1049,8 +1027,8 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne resp.state = static_cast(report_state); resp.volume = media_player->volume; resp.muted = media_player->is_muted(); - fill_entity_state_base(media_player, resp); - return encode_message_to_buffer(resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1067,9 +1045,8 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec media_format.sample_bytes = supported_format.sample_bytes; msg.supported_formats.push_back(media_format); } - msg.unique_id = get_default_unique_id("media_player", media_player); - fill_entity_info_base(media_player, msg); - return encode_message_to_buffer(msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, + remaining_size, is_single); } void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) @@ -1104,9 +1081,8 @@ uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection * bool is_single) { auto *camera = static_cast(entity); ListEntitiesCameraResponse msg; - msg.unique_id = get_default_unique_id("camera", camera); - fill_entity_info_base(camera, msg); - return encode_message_to_buffer(msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::camera_image(const CameraImageRequest &msg) { if (camera::Camera::instance() == nullptr) @@ -1148,9 +1124,9 @@ bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertiseme manufacturer_data.legacy_data.assign(manufacturer_data.data.begin(), manufacturer_data.data.end()); manufacturer_data.data.clear(); } - return this->send_message(resp); + return this->send_message(resp, BluetoothLEAdvertisementResponse::MESSAGE_TYPE); } - return this->send_message(msg); + return this->send_message(msg, BluetoothLEAdvertisementResponse::MESSAGE_TYPE); } void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); @@ -1284,8 +1260,8 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A auto *a_alarm_control_panel = static_cast(entity); AlarmControlPanelStateResponse resp; resp.state = static_cast(a_alarm_control_panel->get_state()); - fill_entity_state_base(a_alarm_control_panel, resp); - return encode_message_to_buffer(resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(a_alarm_control_panel, resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, + remaining_size, is_single); } uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1294,10 +1270,8 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP msg.supported_features = a_alarm_control_panel->get_supported_features(); msg.requires_code = a_alarm_control_panel->get_requires_code(); msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); - msg.unique_id = get_default_unique_id("alarm_control_panel", a_alarm_control_panel); - fill_entity_info_base(a_alarm_control_panel, msg); - return encode_message_to_buffer(msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE, + conn, remaining_size, is_single); } void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) @@ -1338,8 +1312,7 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, const std:: uint32_t remaining_size, bool is_single) { EventResponse resp; resp.event_type = event_type; - fill_entity_state_base(event, resp); - return encode_message_to_buffer(resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -1349,9 +1322,8 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c msg.device_class = event->get_device_class(); for (const auto &event_type : event->get_event_types()) msg.event_types.push_back(event_type); - msg.unique_id = get_default_unique_id("event", event); - fill_entity_info_base(event, msg); - return encode_message_to_buffer(msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } #endif @@ -1377,17 +1349,15 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.release_summary = update->update_info.summary; resp.release_url = update->update_info.release_url; } - fill_entity_state_base(update, resp); - return encode_message_to_buffer(resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *update = static_cast(entity); ListEntitiesUpdateResponse msg; msg.device_class = update->get_device_class(); - msg.unique_id = get_default_unique_id("update", update); - fill_entity_info_base(update, msg); - return encode_message_to_buffer(msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::update_command(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) @@ -1410,9 +1380,6 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { #endif bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) { - if (this->flags_.log_subscription < level) - return false; - // Pre-calculate message size to avoid reallocations uint32_t msg_size = 0; @@ -1435,6 +1402,24 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); } +void APIConnection::complete_authentication_() { + // Early return if already authenticated + if (this->flags_.connection_state == static_cast(ConnectionState::AUTHENTICATED)) { + return; + } + + this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); + ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); +#ifdef USE_API_CLIENT_CONNECTED_TRIGGER + this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); +#endif +#ifdef USE_HOMEASSISTANT_TIME + if (homeassistant::global_homeassistant_time != nullptr) { + this->send_time_request(); + } +#endif +} + HelloResponse APIConnection::hello(const HelloRequest &msg) { this->client_info_ = msg.client_info; this->client_peername_ = this->helper_->getpeername(); @@ -1450,7 +1435,14 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.name = App.get_name(); +#ifdef USE_API_PASSWORD + // Password required - wait for authentication this->flags_.connection_state = static_cast(ConnectionState::CONNECTED); +#else + // No password configured - auto-authenticate + this->complete_authentication_(); +#endif + return resp; } ConnectResponse APIConnection::connect(const ConnectRequest &msg) { @@ -1463,29 +1455,22 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { // bool invalid_password = 1; resp.invalid_password = !correct; if (correct) { - ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); - this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); -#ifdef USE_API_CLIENT_CONNECTED_TRIGGER - this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); -#endif -#ifdef USE_HOMEASSISTANT_TIME - if (homeassistant::global_homeassistant_time != nullptr) { - this->send_time_request(); - } -#endif + this->complete_authentication_(); } return resp; } DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { DeviceInfoResponse resp{}; #ifdef USE_API_PASSWORD - resp.uses_password = this->parent_->uses_password(); + resp.uses_password = true; #else resp.uses_password = false; #endif resp.name = App.get_name(); resp.friendly_name = App.get_friendly_name(); +#ifdef USE_AREAS resp.suggested_area = App.get_area(); +#endif resp.mac_address = get_mac_address_pretty(); resp.esphome_version = ESPHOME_VERSION; resp.compilation_time = App.get_compilation_time(); @@ -1596,7 +1581,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { APIError err = this->helper_->loop(); if (err != APIError::OK) { on_fatal_error(); - ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), + ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), errno); return false; } @@ -1617,12 +1602,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { return false; if (err != APIError::OK) { on_fatal_error(); - if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); - } else { - ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), - api_error_to_str(err), errno); - } + ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), + api_error_to_str(err), errno); return false; } // Do not set last_traffic_ on send @@ -1630,11 +1611,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { } void APIConnection::on_unauthenticated_access() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s requested access without authentication", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str()); } void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str()); } void APIConnection::on_fatal_error() { this->helper_->close(); @@ -1662,8 +1643,15 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { - // Insert at front for high priority messages (no deduplication check) - items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type, estimated_size)); + // Add high priority message and swap to front + // This avoids expensive vector::insert which shifts all elements + // Note: We only ever have one high-priority message at a time (ping OR disconnect) + // If we're disconnecting, pings are blocked, so this simple swap is sufficient + items.emplace_back(entity, std::move(creator), message_type, estimated_size); + if (items.size() > 1) { + // Swap the new high-priority item to the front + std::swap(items.front(), items.back()); + } } bool APIConnection::schedule_batch_() { @@ -1799,12 +1787,8 @@ void APIConnection::process_batch_() { this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { on_fatal_error(); - if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset during batch write", this->get_client_combined_info().c_str()); - } else { - ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), - api_error_to_str(err), errno); - } + ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), + errno); } #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 0051a143de..3873c7fcac 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -111,7 +111,7 @@ class APIConnection : public APIServerConnection { void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { if (!this->flags_.service_call_subscription) return; - this->send_message(call); + this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE); } #ifdef USE_BLUETOOTH_PROXY void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; @@ -133,7 +133,7 @@ class APIConnection : public APIServerConnection { #ifdef USE_HOMEASSISTANT_TIME void send_time_request() { GetTimeRequest req; - this->send_message(req); + this->send_message(req, GetTimeRequest::MESSAGE_TYPE); } #endif @@ -209,6 +209,7 @@ class APIConnection : public APIServerConnection { return static_cast(this->flags_.connection_state) == ConnectionState::CONNECTED || this->is_authenticated(); } + uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; } void on_fatal_error() override; void on_unauthenticated_access() override; void on_no_setup_connection() override; @@ -273,36 +274,43 @@ class APIConnection : public APIServerConnection { ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); protected: - // Helper function to fill common entity info fields - static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) { - // Set common fields that are shared by all entity types - response.key = entity->get_object_id_hash(); - response.object_id = entity->get_object_id(); - - if (entity->has_own_name()) - response.name = entity->get_name(); - - // Set common EntityBase properties - response.icon = entity->get_icon(); - response.disabled_by_default = entity->is_disabled_by_default(); - response.entity_category = static_cast(entity->get_entity_category()); -#ifdef USE_DEVICES - response.device_id = entity->get_device_id(); -#endif - } - - // Helper function to fill common entity state fields - static void fill_entity_state_base(esphome::EntityBase *entity, StateResponseProtoMessage &response) { - response.key = entity->get_object_id_hash(); -#ifdef USE_DEVICES - response.device_id = entity->get_device_id(); -#endif - } + // Helper function to handle authentication completion + void complete_authentication_(); // Non-template helper to encode any ProtoMessage static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, uint32_t remaining_size, bool is_single); + // Helper to fill entity state base and encode message + static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type, + APIConnection *conn, uint32_t remaining_size, bool is_single) { + msg.key = entity->get_object_id_hash(); +#ifdef USE_DEVICES + msg.device_id = entity->get_device_id(); +#endif + return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); + } + + // Helper to fill entity info base and encode message + static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, + 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(); + msg.object_id = entity->get_object_id(); + + if (entity->has_own_name()) + msg.name = entity->get_name(); + + // Set common EntityBase properties + msg.icon = entity->get_icon(); + msg.disabled_by_default = entity->is_disabled_by_default(); + msg.entity_category = static_cast(entity->get_entity_category()); +#ifdef USE_DEVICES + msg.device_id = entity->get_device_id(); +#endif + return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); + } + #ifdef USE_VOICE_ASSISTANT // Helper to check voice assistant validity and connection ownership inline bool check_voice_assistant_api_connection_() const; diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 3a547b8688..bb3947e8a3 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -23,3 +23,8 @@ extend google.protobuf.MessageOptions { optional bool no_delay = 1040 [default=false]; optional string base_class = 1041; } + +extend google.protobuf.FieldOptions { + optional string field_ifdef = 1042; + optional uint32 fixed_array_size = 50007; +} diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 4c0e20e0f0..437c9ece1d 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3,33 +3,33 @@ #include "api_pb2.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include namespace esphome { namespace api { bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->api_version_major = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->api_version_minor = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->client_info = value.as_string(); - return true; - } + break; default: return false; } + return true; } void HelloResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->api_version_major); @@ -45,38 +45,18 @@ void HelloResponse::calculate_size(uint32_t &total_size) const { } bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->password = value.as_string(); - return true; - } + break; default: return false; } + return true; } void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); } void ConnectResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->invalid_password); } -bool AreaInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->area_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool AreaInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->name = value.as_string(); - return true; - } - default: - return false; - } -} void AreaInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->area_id); buffer.encode_string(2, this->name); @@ -85,30 +65,6 @@ void AreaInfo::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->area_id); ProtoSize::add_string_field(total_size, 1, this->name); } -bool DeviceInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->device_id = value.as_uint32(); - return true; - } - case 3: { - this->area_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool DeviceInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->name = value.as_string(); - return true; - } - default: - return false; - } -} void DeviceInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->device_id); buffer.encode_string(2, this->name); @@ -126,26 +82,54 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(4, this->esphome_version); buffer.encode_string(5, this->compilation_time); buffer.encode_string(6, this->model); +#ifdef USE_DEEP_SLEEP buffer.encode_bool(7, this->has_deep_sleep); +#endif +#ifdef ESPHOME_PROJECT_NAME buffer.encode_string(8, this->project_name); +#endif +#ifdef ESPHOME_PROJECT_NAME buffer.encode_string(9, this->project_version); +#endif +#ifdef USE_WEBSERVER buffer.encode_uint32(10, this->webserver_port); +#endif +#ifdef USE_BLUETOOTH_PROXY buffer.encode_uint32(11, this->legacy_bluetooth_proxy_version); +#endif +#ifdef USE_BLUETOOTH_PROXY buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags); +#endif buffer.encode_string(12, this->manufacturer); buffer.encode_string(13, this->friendly_name); +#ifdef USE_VOICE_ASSISTANT buffer.encode_uint32(14, this->legacy_voice_assistant_version); +#endif +#ifdef USE_VOICE_ASSISTANT buffer.encode_uint32(17, this->voice_assistant_feature_flags); +#endif +#ifdef USE_AREAS buffer.encode_string(16, this->suggested_area); +#endif +#ifdef USE_BLUETOOTH_PROXY buffer.encode_string(18, this->bluetooth_mac_address); +#endif +#ifdef USE_API_NOISE buffer.encode_bool(19, this->api_encryption_supported); +#endif +#ifdef USE_DEVICES for (auto &it : this->devices) { buffer.encode_message(20, it, true); } +#endif +#ifdef USE_AREAS for (auto &it : this->areas) { buffer.encode_message(21, it, true); } +#endif +#ifdef USE_AREAS buffer.encode_message(22, this->area); +#endif } void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->uses_password); @@ -154,59 +138,97 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->esphome_version); ProtoSize::add_string_field(total_size, 1, this->compilation_time); ProtoSize::add_string_field(total_size, 1, this->model); +#ifdef USE_DEEP_SLEEP ProtoSize::add_bool_field(total_size, 1, this->has_deep_sleep); +#endif +#ifdef ESPHOME_PROJECT_NAME ProtoSize::add_string_field(total_size, 1, this->project_name); +#endif +#ifdef ESPHOME_PROJECT_NAME ProtoSize::add_string_field(total_size, 1, this->project_version); +#endif +#ifdef USE_WEBSERVER ProtoSize::add_uint32_field(total_size, 1, this->webserver_port); +#endif +#ifdef USE_BLUETOOTH_PROXY ProtoSize::add_uint32_field(total_size, 1, this->legacy_bluetooth_proxy_version); +#endif +#ifdef USE_BLUETOOTH_PROXY ProtoSize::add_uint32_field(total_size, 1, this->bluetooth_proxy_feature_flags); +#endif ProtoSize::add_string_field(total_size, 1, this->manufacturer); ProtoSize::add_string_field(total_size, 1, this->friendly_name); +#ifdef USE_VOICE_ASSISTANT ProtoSize::add_uint32_field(total_size, 1, this->legacy_voice_assistant_version); +#endif +#ifdef USE_VOICE_ASSISTANT ProtoSize::add_uint32_field(total_size, 2, this->voice_assistant_feature_flags); +#endif +#ifdef USE_AREAS ProtoSize::add_string_field(total_size, 2, this->suggested_area); +#endif +#ifdef USE_BLUETOOTH_PROXY ProtoSize::add_string_field(total_size, 2, this->bluetooth_mac_address); +#endif +#ifdef USE_API_NOISE ProtoSize::add_bool_field(total_size, 2, this->api_encryption_supported); +#endif +#ifdef USE_DEVICES ProtoSize::add_repeated_message(total_size, 2, this->devices); +#endif +#ifdef USE_AREAS ProtoSize::add_repeated_message(total_size, 2, this->areas); +#endif +#ifdef USE_AREAS ProtoSize::add_message_object(total_size, 2, this->area); +#endif } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); buffer.encode_bool(7, this->disabled_by_default); +#ifdef USE_ENTITY_ICON buffer.encode_string(8, this->icon); +#endif buffer.encode_uint32(9, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); +#endif } void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_string_field(total_size, 1, this->device_class); ProtoSize::add_bool_field(total_size, 1, this->is_status_binary_sensor); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } void BinarySensorStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->missing_state); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } #endif #ifdef USE_COVER @@ -214,31 +236,37 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->assumed_state); buffer.encode_bool(6, this->supports_position); buffer.encode_bool(7, this->supports_tilt); buffer.encode_string(8, this->device_class); buffer.encode_bool(9, this->disabled_by_default); +#ifdef USE_ENTITY_ICON buffer.encode_string(10, this->icon); +#endif buffer.encode_uint32(11, static_cast(this->entity_category)); buffer.encode_bool(12, this->supports_stop); +#ifdef USE_DEVICES buffer.encode_uint32(13, this->device_id); +#endif } void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_bool_field(total_size, 1, this->assumed_state); ProtoSize::add_bool_field(total_size, 1, this->supports_position); ProtoSize::add_bool_field(total_size, 1, this->supports_tilt); ProtoSize::add_string_field(total_size, 1, this->device_class); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_bool_field(total_size, 1, this->supports_stop); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -246,63 +274,62 @@ void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); buffer.encode_uint32(5, static_cast(this->current_operation)); +#ifdef USE_DEVICES buffer.encode_uint32(6, this->device_id); +#endif } void CoverStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_state)); - ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->position); + ProtoSize::add_float_field(total_size, 1, this->tilt); ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_legacy_command = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->legacy_command = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; + case 4: this->has_position = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_tilt = value.as_bool(); - return true; - } - case 8: { + break; + case 8: this->stop = value.as_bool(); - return true; - } - case 9: { + break; +#ifdef USE_DEVICES + case 9: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 5: { + break; + case 5: this->position = value.as_float(); - return true; - } - case 7: { + break; + case 7: this->tilt = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_FAN @@ -310,37 +337,43 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); buffer.encode_bool(7, this->supports_direction); buffer.encode_int32(8, this->supported_speed_count); buffer.encode_bool(9, this->disabled_by_default); +#ifdef USE_ENTITY_ICON buffer.encode_string(10, this->icon); +#endif buffer.encode_uint32(11, static_cast(this->entity_category)); for (auto &it : this->supported_preset_modes) { buffer.encode_string(12, it, true); } +#ifdef USE_DEVICES buffer.encode_uint32(13, this->device_id); +#endif } void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_bool_field(total_size, 1, this->supports_oscillation); ProtoSize::add_bool_field(total_size, 1, this->supports_speed); ProtoSize::add_bool_field(total_size, 1, this->supports_direction); ProtoSize::add_int32_field(total_size, 1, this->supported_speed_count); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); if (!this->supported_preset_modes.empty()) { for (const auto &it : this->supported_preset_modes) { ProtoSize::add_string_field_repeated(total_size, 1, it); } } +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -350,91 +383,86 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(5, static_cast(this->direction)); buffer.encode_int32(6, this->speed_level); buffer.encode_string(7, this->preset_mode); +#ifdef USE_DEVICES buffer.encode_uint32(8, this->device_id); +#endif } void FanStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->oscillating); ProtoSize::add_enum_field(total_size, 1, static_cast(this->speed)); ProtoSize::add_enum_field(total_size, 1, static_cast(this->direction)); ProtoSize::add_int32_field(total_size, 1, this->speed_level); ProtoSize::add_string_field(total_size, 1, this->preset_mode); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_state = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->state = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->has_speed = value.as_bool(); - return true; - } - case 5: { + break; + case 5: this->speed = static_cast(value.as_uint32()); - return true; - } - case 6: { + break; + case 6: this->has_oscillating = value.as_bool(); - return true; - } - case 7: { + break; + case 7: this->oscillating = value.as_bool(); - return true; - } - case 8: { + break; + case 8: this->has_direction = value.as_bool(); - return true; - } - case 9: { + break; + case 9: this->direction = static_cast(value.as_uint32()); - return true; - } - case 10: { + break; + case 10: this->has_speed_level = value.as_bool(); - return true; - } - case 11: { + break; + case 11: this->speed_level = value.as_int32(); - return true; - } - case 12: { + break; + case 12: this->has_preset_mode = value.as_bool(); - return true; - } - case 14: { + break; +#ifdef USE_DEVICES + case 14: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 13: { + case 13: this->preset_mode = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_LIGHT @@ -442,7 +470,6 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); for (auto &it : this->supported_color_modes) { buffer.encode_uint32(12, static_cast(it), true); } @@ -456,15 +483,18 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(11, it, true); } buffer.encode_bool(13, this->disabled_by_default); +#ifdef USE_ENTITY_ICON buffer.encode_string(14, this->icon); +#endif buffer.encode_uint32(15, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(16, this->device_id); +#endif } void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); if (!this->supported_color_modes.empty()) { for (const auto &it : this->supported_color_modes) { ProtoSize::add_enum_field_repeated(total_size, 1, static_cast(it)); @@ -474,17 +504,21 @@ void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_rgb); ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_white_value); ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_color_temperature); - ProtoSize::add_fixed_field<4>(total_size, 1, this->min_mireds != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->max_mireds != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->min_mireds); + ProtoSize::add_float_field(total_size, 1, this->max_mireds); if (!this->effects.empty()) { for (const auto &it : this->effects) { ProtoSize::add_string_field_repeated(total_size, 1, it); } } ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 2, this->device_id); +#endif } void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -500,153 +534,134 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(12, this->cold_white); buffer.encode_float(13, this->warm_white); buffer.encode_string(9, this->effect); +#ifdef USE_DEVICES buffer.encode_uint32(14, this->device_id); +#endif } void LightStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->state); - ProtoSize::add_fixed_field<4>(total_size, 1, this->brightness != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->brightness); ProtoSize::add_enum_field(total_size, 1, static_cast(this->color_mode)); - ProtoSize::add_fixed_field<4>(total_size, 1, this->color_brightness != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->red != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->green != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->blue != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->white != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->color_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->cold_white != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->warm_white != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->color_brightness); + ProtoSize::add_float_field(total_size, 1, this->red); + ProtoSize::add_float_field(total_size, 1, this->green); + ProtoSize::add_float_field(total_size, 1, this->blue); + ProtoSize::add_float_field(total_size, 1, this->white); + ProtoSize::add_float_field(total_size, 1, this->color_temperature); + ProtoSize::add_float_field(total_size, 1, this->cold_white); + ProtoSize::add_float_field(total_size, 1, this->warm_white); ProtoSize::add_string_field(total_size, 1, this->effect); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_state = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->state = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->has_brightness = value.as_bool(); - return true; - } - case 22: { + break; + case 22: this->has_color_mode = value.as_bool(); - return true; - } - case 23: { + break; + case 23: this->color_mode = static_cast(value.as_uint32()); - return true; - } - case 20: { + break; + case 20: this->has_color_brightness = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_rgb = value.as_bool(); - return true; - } - case 10: { + break; + case 10: this->has_white = value.as_bool(); - return true; - } - case 12: { + break; + case 12: this->has_color_temperature = value.as_bool(); - return true; - } - case 24: { + break; + case 24: this->has_cold_white = value.as_bool(); - return true; - } - case 26: { + break; + case 26: this->has_warm_white = value.as_bool(); - return true; - } - case 14: { + break; + case 14: this->has_transition_length = value.as_bool(); - return true; - } - case 15: { + break; + case 15: this->transition_length = value.as_uint32(); - return true; - } - case 16: { + break; + case 16: this->has_flash_length = value.as_bool(); - return true; - } - case 17: { + break; + case 17: this->flash_length = value.as_uint32(); - return true; - } - case 18: { + break; + case 18: this->has_effect = value.as_bool(); - return true; - } - case 28: { + break; +#ifdef USE_DEVICES + case 28: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 19: { + case 19: this->effect = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 5: { + break; + case 5: this->brightness = value.as_float(); - return true; - } - case 21: { + break; + case 21: this->color_brightness = value.as_float(); - return true; - } - case 7: { + break; + case 7: this->red = value.as_float(); - return true; - } - case 8: { + break; + case 8: this->green = value.as_float(); - return true; - } - case 9: { + break; + case 9: this->blue = value.as_float(); - return true; - } - case 11: { + break; + case 11: this->white = value.as_float(); - return true; - } - case 13: { + break; + case 13: this->color_temperature = value.as_float(); - return true; - } - case 25: { + break; + case 25: this->cold_white = value.as_float(); - return true; - } - case 27: { + break; + case 27: this->warm_white = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_SENSOR @@ -654,8 +669,9 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_string(6, this->unit_of_measurement); buffer.encode_int32(7, this->accuracy_decimals); buffer.encode_bool(8, this->force_update); @@ -664,14 +680,17 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(11, static_cast(this->legacy_last_reset_type)); buffer.encode_bool(12, this->disabled_by_default); buffer.encode_uint32(13, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(14, this->device_id); +#endif } void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement); ProtoSize::add_int32_field(total_size, 1, this->accuracy_decimals); ProtoSize::add_bool_field(total_size, 1, this->force_update); @@ -680,19 +699,25 @@ void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_last_reset_type)); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } void SensorStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_float_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->missing_state); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } #endif #ifdef USE_SWITCH @@ -700,59 +725,70 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->assumed_state); buffer.encode_bool(7, this->disabled_by_default); buffer.encode_uint32(8, static_cast(this->entity_category)); buffer.encode_string(9, this->device_class); +#ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); +#endif } void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->assumed_state); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_string_field(total_size, 1, this->device_class); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); +#ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); +#endif } void SwitchStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->state); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->state = value.as_bool(); - return true; - } - case 3: { + break; +#ifdef USE_DEVICES + case 3: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_TEXT_SENSOR @@ -760,50 +796,59 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_string(8, this->device_class); +#ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); +#endif } void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_string_field(total_size, 1, this->device_class); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } void TextSensorStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->missing_state); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } #endif bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->level = static_cast(value.as_uint32()); - return true; - } - case 2: { + break; + case 2: this->dump_config = value.as_bool(); - return true; - } + break; default: return false; } + return true; } void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, static_cast(this->level)); @@ -818,33 +863,19 @@ void SubscribeLogsResponse::calculate_size(uint32_t &total_size) const { #ifdef USE_API_NOISE bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_string(); - return true; - } + break; default: return false; } + return true; } void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } void NoiseEncryptionSetKeyResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->success); } #endif -bool HomeassistantServiceMap::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->key = value.as_string(); - return true; - } - case 2: { - this->value = value.as_string(); - return true; - } - default: - return false; - } -} void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); @@ -885,57 +916,35 @@ void SubscribeHomeAssistantStateResponse::calculate_size(uint32_t &total_size) c } bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->entity_id = value.as_string(); - return true; - } - case 2: { + break; + case 2: this->state = value.as_string(); - return true; - } - case 3: { + break; + case 3: this->attribute = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->epoch_seconds = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); } void GetTimeResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->epoch_seconds); } #ifdef USE_API_SERVICES -bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->type = static_cast(value.as_uint32()); - return true; - } - default: - return false; - } -} -bool ListEntitiesServicesArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->name = value.as_string(); - return true; - } - default: - return false; - } -} void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name); buffer.encode_uint32(2, static_cast(this->type)); @@ -953,127 +962,77 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { } void ListEntitiesServicesResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_repeated_message(total_size, 1, this->args); } bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->bool_ = value.as_bool(); - return true; - } - case 2: { + break; + case 2: this->legacy_int = value.as_int32(); - return true; - } - case 5: { + break; + case 5: this->int_ = value.as_sint32(); - return true; - } - case 6: { + break; + case 6: this->bool_array.push_back(value.as_bool()); - return true; - } - case 7: { + break; + case 7: this->int_array.push_back(value.as_sint32()); - return true; - } + break; default: return false; } + return true; } bool ExecuteServiceArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: { + case 4: this->string_ = value.as_string(); - return true; - } - case 9: { + break; + case 9: this->string_array.push_back(value.as_string()); - return true; - } + break; default: return false; } + return true; } bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 3: { + case 3: this->float_ = value.as_float(); - return true; - } - case 8: { + break; + case 8: this->float_array.push_back(value.as_float()); - return true; - } + break; default: return false; } -} -void ExecuteServiceArgument::encode(ProtoWriteBuffer buffer) const { - buffer.encode_bool(1, this->bool_); - buffer.encode_int32(2, this->legacy_int); - buffer.encode_float(3, this->float_); - buffer.encode_string(4, this->string_); - buffer.encode_sint32(5, this->int_); - for (auto it : this->bool_array) { - buffer.encode_bool(6, it, true); - } - for (auto &it : this->int_array) { - buffer.encode_sint32(7, it, true); - } - for (auto &it : this->float_array) { - buffer.encode_float(8, it, true); - } - for (auto &it : this->string_array) { - buffer.encode_string(9, it, true); - } -} -void ExecuteServiceArgument::calculate_size(uint32_t &total_size) const { - ProtoSize::add_bool_field(total_size, 1, this->bool_); - ProtoSize::add_int32_field(total_size, 1, this->legacy_int); - ProtoSize::add_fixed_field<4>(total_size, 1, this->float_ != 0.0f); - ProtoSize::add_string_field(total_size, 1, this->string_); - ProtoSize::add_sint32_field(total_size, 1, this->int_); - if (!this->bool_array.empty()) { - for (const auto it : this->bool_array) { - ProtoSize::add_bool_field_repeated(total_size, 1, it); - } - } - if (!this->int_array.empty()) { - for (const auto &it : this->int_array) { - ProtoSize::add_sint32_field_repeated(total_size, 1, it); - } - } - if (!this->float_array.empty()) { - total_size += this->float_array.size() * 5; - } - if (!this->string_array.empty()) { - for (const auto &it : this->string_array) { - ProtoSize::add_string_field_repeated(total_size, 1, it); - } - } + return true; } bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->args.emplace_back(); value.decode_to_message(this->args.back()); - return true; - } + break; default: return false; } + return true; } bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_CAMERA @@ -1081,47 +1040,56 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->disabled_by_default); +#ifdef USE_ENTITY_ICON buffer.encode_string(6, this->icon); +#endif buffer.encode_uint32(7, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(8, this->device_id); +#endif } void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bytes(2, reinterpret_cast(this->data.data()), this->data.size()); buffer.encode_bool(3, this->done); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } void CameraImageResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->data); ProtoSize::add_bool_field(total_size, 1, this->done); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->single = value.as_bool(); - return true; - } - case 2: { + break; + case 2: this->stream = value.as_bool(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_CLIMATE @@ -1129,7 +1097,6 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); for (auto &it : this->supported_modes) { @@ -1156,20 +1123,23 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(17, it, true); } buffer.encode_bool(18, this->disabled_by_default); +#ifdef USE_ENTITY_ICON buffer.encode_string(19, this->icon); +#endif buffer.encode_uint32(20, static_cast(this->entity_category)); buffer.encode_float(21, this->visual_current_temperature_step); buffer.encode_bool(22, this->supports_current_humidity); buffer.encode_bool(23, this->supports_target_humidity); buffer.encode_float(24, this->visual_min_humidity); buffer.encode_float(25, this->visual_max_humidity); +#ifdef USE_DEVICES buffer.encode_uint32(26, this->device_id); +#endif } void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); ProtoSize::add_bool_field(total_size, 1, this->supports_current_temperature); ProtoSize::add_bool_field(total_size, 1, this->supports_two_point_target_temperature); if (!this->supported_modes.empty()) { @@ -1177,9 +1147,9 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_enum_field_repeated(total_size, 1, static_cast(it)); } } - ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_min_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_max_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_target_temperature_step != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->visual_min_temperature); + ProtoSize::add_float_field(total_size, 1, this->visual_max_temperature); + ProtoSize::add_float_field(total_size, 1, this->visual_target_temperature_step); ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_away); ProtoSize::add_bool_field(total_size, 1, this->supports_action); if (!this->supported_fan_modes.empty()) { @@ -1208,14 +1178,18 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { } } ProtoSize::add_bool_field(total_size, 2, this->disabled_by_default); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 2, this->icon); +#endif ProtoSize::add_enum_field(total_size, 2, static_cast(this->entity_category)); - ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_current_temperature_step != 0.0f); + ProtoSize::add_float_field(total_size, 2, this->visual_current_temperature_step); ProtoSize::add_bool_field(total_size, 2, this->supports_current_humidity); ProtoSize::add_bool_field(total_size, 2, this->supports_target_humidity); - ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_min_humidity != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_max_humidity != 0.0f); + ProtoSize::add_float_field(total_size, 2, this->visual_min_humidity); + ProtoSize::add_float_field(total_size, 2, this->visual_max_humidity); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 2, this->device_id); +#endif } void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -1233,15 +1207,17 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(13, this->custom_preset); buffer.encode_float(14, this->current_humidity); buffer.encode_float(15, this->target_humidity); +#ifdef USE_DEVICES buffer.encode_uint32(16, this->device_id); +#endif } void ClimateStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode)); - ProtoSize::add_fixed_field<4>(total_size, 1, this->current_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_low != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_high != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->current_temperature); + ProtoSize::add_float_field(total_size, 1, this->target_temperature); + ProtoSize::add_float_field(total_size, 1, this->target_temperature_low); + ProtoSize::add_float_field(total_size, 1, this->target_temperature_high); ProtoSize::add_bool_field(total_size, 1, this->unused_legacy_away); ProtoSize::add_enum_field(total_size, 1, static_cast(this->action)); ProtoSize::add_enum_field(total_size, 1, static_cast(this->fan_mode)); @@ -1249,123 +1225,106 @@ void ClimateStateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->custom_fan_mode); ProtoSize::add_enum_field(total_size, 1, static_cast(this->preset)); ProtoSize::add_string_field(total_size, 1, this->custom_preset); - ProtoSize::add_fixed_field<4>(total_size, 1, this->current_humidity != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_humidity != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->current_humidity); + ProtoSize::add_float_field(total_size, 1, this->target_humidity); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 2, this->device_id); +#endif } bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_mode = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->mode = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; + case 4: this->has_target_temperature = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_target_temperature_low = value.as_bool(); - return true; - } - case 8: { + break; + case 8: this->has_target_temperature_high = value.as_bool(); - return true; - } - case 10: { + break; + case 10: this->unused_has_legacy_away = value.as_bool(); - return true; - } - case 11: { + break; + case 11: this->unused_legacy_away = value.as_bool(); - return true; - } - case 12: { + break; + case 12: this->has_fan_mode = value.as_bool(); - return true; - } - case 13: { + break; + case 13: this->fan_mode = static_cast(value.as_uint32()); - return true; - } - case 14: { + break; + case 14: this->has_swing_mode = value.as_bool(); - return true; - } - case 15: { + break; + case 15: this->swing_mode = static_cast(value.as_uint32()); - return true; - } - case 16: { + break; + case 16: this->has_custom_fan_mode = value.as_bool(); - return true; - } - case 18: { + break; + case 18: this->has_preset = value.as_bool(); - return true; - } - case 19: { + break; + case 19: this->preset = static_cast(value.as_uint32()); - return true; - } - case 20: { + break; + case 20: this->has_custom_preset = value.as_bool(); - return true; - } - case 22: { + break; + case 22: this->has_target_humidity = value.as_bool(); - return true; - } - case 24: { + break; +#ifdef USE_DEVICES + case 24: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 17: { + case 17: this->custom_fan_mode = value.as_string(); - return true; - } - case 21: { + break; + case 21: this->custom_preset = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 5: { + break; + case 5: this->target_temperature = value.as_float(); - return true; - } - case 7: { + break; + case 7: this->target_temperature_low = value.as_float(); - return true; - } - case 9: { + break; + case 9: this->target_temperature_high = value.as_float(); - return true; - } - case 23: { + break; + case 23: this->target_humidity = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_NUMBER @@ -1373,8 +1332,9 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_float(6, this->min_value); buffer.encode_float(7, this->max_value); buffer.encode_float(8, this->step); @@ -1383,59 +1343,69 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(11, this->unit_of_measurement); buffer.encode_uint32(12, static_cast(this->mode)); buffer.encode_string(13, this->device_class); +#ifdef USE_DEVICES buffer.encode_uint32(14, this->device_id); +#endif } void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); - ProtoSize::add_fixed_field<4>(total_size, 1, this->min_value != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->max_value != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->step != 0.0f); +#endif + ProtoSize::add_float_field(total_size, 1, this->min_value); + ProtoSize::add_float_field(total_size, 1, this->max_value); + ProtoSize::add_float_field(total_size, 1, this->step); ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode)); ProtoSize::add_string_field(total_size, 1, this->device_class); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } void NumberStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_float_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->missing_state); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool NumberCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 3: { +#ifdef USE_DEVICES + case 3: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 2: { + break; + case 2: this->state = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_SELECT @@ -1443,21 +1413,25 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif for (auto &it : this->options) { buffer.encode_string(6, it, true); } buffer.encode_bool(7, this->disabled_by_default); buffer.encode_uint32(8, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); +#endif } void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif if (!this->options.empty()) { for (const auto &it : this->options) { ProtoSize::add_string_field_repeated(total_size, 1, it); @@ -1465,49 +1439,57 @@ void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { } ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } void SelectStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->missing_state); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 3: { +#ifdef USE_DEVICES + case 3: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->state = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_SIREN @@ -1515,8 +1497,9 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); for (auto &it : this->tones) { buffer.encode_string(7, it, true); @@ -1524,14 +1507,17 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(8, this->supports_duration); buffer.encode_bool(9, this->supports_volume); buffer.encode_uint32(10, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(11, this->device_id); +#endif } void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); if (!this->tones.empty()) { for (const auto &it : this->tones) { @@ -1541,75 +1527,76 @@ void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->supports_duration); ProtoSize::add_bool_field(total_size, 1, this->supports_volume); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void SirenStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); +#ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); +#endif } void SirenStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->state); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_state = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->state = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->has_tone = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_duration = value.as_bool(); - return true; - } - case 7: { + break; + case 7: this->duration = value.as_uint32(); - return true; - } - case 8: { + break; + case 8: this->has_volume = value.as_bool(); - return true; - } - case 10: { + break; +#ifdef USE_DEVICES + case 10: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool SirenCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 5: { + case 5: this->tone = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 9: { + break; + case 9: this->volume = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_LOCK @@ -1617,77 +1604,87 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_bool(8, this->assumed_state); buffer.encode_bool(9, this->supports_open); buffer.encode_bool(10, this->requires_code); buffer.encode_string(11, this->code_format); +#ifdef USE_DEVICES buffer.encode_uint32(12, this->device_id); +#endif } void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_bool_field(total_size, 1, this->assumed_state); ProtoSize::add_bool_field(total_size, 1, this->supports_open); ProtoSize::add_bool_field(total_size, 1, this->requires_code); ProtoSize::add_string_field(total_size, 1, this->code_format); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void LockStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); +#ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); +#endif } void LockStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_enum_field(total_size, 1, static_cast(this->state)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->command = static_cast(value.as_uint32()); - return true; - } - case 3: { + break; + case 3: this->has_code = value.as_bool(); - return true; - } - case 5: { + break; +#ifdef USE_DEVICES + case 5: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool LockCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: { + case 4: this->code = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_BUTTON @@ -1695,78 +1692,54 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_string(8, this->device_class); +#ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); +#endif } void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_string_field(total_size, 1, this->device_class); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool ButtonCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { +#ifdef USE_DEVICES + case 2: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_MEDIA_PLAYER -bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->sample_rate = value.as_uint32(); - return true; - } - case 3: { - this->num_channels = value.as_uint32(); - return true; - } - case 4: { - this->purpose = static_cast(value.as_uint32()); - return true; - } - case 5: { - this->sample_bytes = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool MediaPlayerSupportedFormat::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->format = value.as_string(); - return true; - } - default: - return false; - } -} void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->format); buffer.encode_uint32(2, this->sample_rate); @@ -1785,135 +1758,116 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_bool(8, this->supports_pause); for (auto &it : this->supported_formats) { buffer.encode_message(9, it, true); } +#ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); +#endif } void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_bool_field(total_size, 1, this->supports_pause); ProtoSize::add_repeated_message(total_size, 1, this->supported_formats); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); buffer.encode_float(3, this->volume); buffer.encode_bool(4, this->muted); +#ifdef USE_DEVICES buffer.encode_uint32(5, this->device_id); +#endif } void MediaPlayerStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_enum_field(total_size, 1, static_cast(this->state)); - ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->volume); ProtoSize::add_bool_field(total_size, 1, this->muted); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_command = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->command = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; + case 4: this->has_volume = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_media_url = value.as_bool(); - return true; - } - case 8: { + break; + case 8: this->has_announcement = value.as_bool(); - return true; - } - case 9: { + break; + case 9: this->announcement = value.as_bool(); - return true; - } - case 10: { + break; +#ifdef USE_DEVICES + case 10: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 7: { + case 7: this->media_url = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 5: { + break; + case 5: this->volume = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_BLUETOOTH_PROXY bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->flags = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->legacy_data.push_back(value.as_uint32()); - return true; - } - default: - return false; - } -} -bool BluetoothServiceData::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->uuid = value.as_string(); - return true; - } - case 3: { - this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } void BluetoothServiceData::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->uuid); @@ -1959,45 +1913,19 @@ void BluetoothLEAdvertisementResponse::calculate_size(uint32_t &total_size) cons ProtoSize::add_repeated_message(total_size, 1, this->manufacturer_data); ProtoSize::add_uint32_field(total_size, 1, this->address_type); } -bool BluetoothLERawAdvertisement::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - case 2: { - this->rssi = value.as_sint32(); - return true; - } - case 3: { - this->address_type = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool BluetoothLERawAdvertisement::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 4: { - this->data = value.as_string(); - return true; - } - default: - return false; - } -} void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_sint32(2, this->rssi); buffer.encode_uint32(3, this->address_type); - buffer.encode_bytes(4, reinterpret_cast(this->data.data()), this->data.size()); + buffer.encode_bytes(4, this->data, this->data_len); } void BluetoothLERawAdvertisement::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address); ProtoSize::add_sint32_field(total_size, 1, this->rssi); ProtoSize::add_uint32_field(total_size, 1, this->address_type); - ProtoSize::add_string_field(total_size, 1, this->data); + if (this->data_len != 0) { + total_size += 1 + ProtoSize::varint(static_cast(this->data_len)) + this->data_len; + } } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->advertisements) { @@ -2009,25 +1937,22 @@ void BluetoothLERawAdvertisementsResponse::calculate_size(uint32_t &total_size) } bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->request_type = static_cast(value.as_uint32()); - return true; - } - case 3: { + break; + case 3: this->has_address_type = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->address_type = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -2043,27 +1968,13 @@ void BluetoothDeviceConnectionResponse::calculate_size(uint32_t &total_size) con } bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - default: - return false; - } -} -bool BluetoothGATTDescriptor::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->uuid.push_back(value.as_uint64()); - return true; - } - case 2: { - this->handle = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->uuid) { @@ -2079,35 +1990,6 @@ void BluetoothGATTDescriptor::calculate_size(uint32_t &total_size) const { } ProtoSize::add_uint32_field(total_size, 1, this->handle); } -bool BluetoothGATTCharacteristic::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->uuid.push_back(value.as_uint64()); - return true; - } - case 2: { - this->handle = value.as_uint32(); - return true; - } - case 3: { - this->properties = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool BluetoothGATTCharacteristic::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 4: { - this->descriptors.emplace_back(); - value.decode_to_message(this->descriptors.back()); - return true; - } - default: - return false; - } -} void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->uuid) { buffer.encode_uint64(1, it, true); @@ -2128,31 +2010,6 @@ void BluetoothGATTCharacteristic::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->properties); ProtoSize::add_repeated_message(total_size, 1, this->descriptors); } -bool BluetoothGATTService::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->uuid.push_back(value.as_uint64()); - return true; - } - case 2: { - this->handle = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool BluetoothGATTService::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 3: { - this->characteristics.emplace_back(); - value.decode_to_message(this->characteristics.back()); - return true; - } - default: - return false; - } -} void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->uuid) { buffer.encode_uint64(1, it, true); @@ -2189,17 +2046,16 @@ void BluetoothGATTGetServicesDoneResponse::calculate_size(uint32_t &total_size) } bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -2213,87 +2069,81 @@ void BluetoothGATTReadResponse::calculate_size(uint32_t &total_size) const { } bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->response = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: { + case 4: this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTReadDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 3: { + case 3: this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->enable = value.as_bool(); - return true; - } + break; default: return false; } + return true; } void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -2387,53 +2237,28 @@ void BluetoothScannerStateResponse::calculate_size(uint32_t &total_size) const { } bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->mode = static_cast(value.as_uint32()); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_VOICE_ASSISTANT bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->subscribe = value.as_bool(); - return true; - } - case 2: { + break; + case 2: this->flags = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool VoiceAssistantAudioSettings::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->noise_suppression_level = value.as_uint32(); - return true; - } - case 2: { - this->auto_gain = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool VoiceAssistantAudioSettings::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 3: { - this->volume_multiplier = value.as_float(); - return true; - } + break; default: return false; } + return true; } void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->noise_suppression_level); @@ -2443,7 +2268,7 @@ void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { void VoiceAssistantAudioSettings::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint32_field(total_size, 1, this->noise_suppression_level); ProtoSize::add_uint32_field(total_size, 1, this->auto_gain); - ProtoSize::add_fixed_field<4>(total_size, 1, this->volume_multiplier != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->volume_multiplier); } void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->start); @@ -2461,80 +2286,70 @@ void VoiceAssistantRequest::calculate_size(uint32_t &total_size) const { } bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->port = value.as_uint32(); - return true; - } - case 2: { + break; + case 2: this->error = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantEventData::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->name = value.as_string(); - return true; - } - case 2: { + break; + case 2: this->value = value.as_string(); - return true; - } + break; default: return false; } -} -void VoiceAssistantEventData::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->name); - buffer.encode_string(2, this->value); -} -void VoiceAssistantEventData::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->value); + return true; } bool VoiceAssistantEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->event_type = static_cast(value.as_uint32()); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->data.emplace_back(); value.decode_to_message(this->data.back()); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAudio::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->end = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(1, reinterpret_cast(this->data.data()), this->data.size()); @@ -2546,90 +2361,66 @@ void VoiceAssistantAudio::calculate_size(uint32_t &total_size) const { } bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->event_type = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; + case 4: this->total_seconds = value.as_uint32(); - return true; - } - case 5: { + break; + case 5: this->seconds_left = value.as_uint32(); - return true; - } - case 6: { + break; + case 6: this->is_active = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantTimerEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->timer_id = value.as_string(); - return true; - } - case 3: { + break; + case 3: this->name = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 4: { + case 4: this->start_conversation = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->media_id = value.as_string(); - return true; - } - case 2: { + break; + case 2: this->text = value.as_string(); - return true; - } - case 3: { + break; + case 3: this->preannounce_media_id = value.as_string(); - return true; - } + break; default: return false; } + return true; } void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } void VoiceAssistantAnnounceFinished::calculate_size(uint32_t &total_size) const { ProtoSize::add_bool_field(total_size, 1, this->success); } -bool VoiceAssistantWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->id = value.as_string(); - return true; - } - case 2: { - this->wake_word = value.as_string(); - return true; - } - case 3: { - this->trained_languages.push_back(value.as_string()); - return true; - } - default: - return false; - } -} void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->id); buffer.encode_string(2, this->wake_word); @@ -2666,13 +2457,13 @@ void VoiceAssistantConfigurationResponse::calculate_size(uint32_t &total_size) c } bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->active_wake_words.push_back(value.as_string()); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_ALARM_CONTROL_PANEL @@ -2680,71 +2471,82 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) cons buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_uint32(8, this->supported_features); buffer.encode_bool(9, this->requires_code); buffer.encode_bool(10, this->requires_code_to_arm); +#ifdef USE_DEVICES buffer.encode_uint32(11, this->device_id); +#endif } void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_uint32_field(total_size, 1, this->supported_features); ProtoSize::add_bool_field(total_size, 1, this->requires_code); ProtoSize::add_bool_field(total_size, 1, this->requires_code_to_arm); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); +#ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); +#endif } void AlarmControlPanelStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_enum_field(total_size, 1, static_cast(this->state)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->command = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; +#ifdef USE_DEVICES + case 4: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool AlarmControlPanelCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 3: { + case 3: this->code = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_TEXT @@ -2752,71 +2554,83 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_uint32(8, this->min_length); buffer.encode_uint32(9, this->max_length); buffer.encode_string(10, this->pattern); buffer.encode_uint32(11, static_cast(this->mode)); +#ifdef USE_DEVICES buffer.encode_uint32(12, this->device_id); +#endif } void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_uint32_field(total_size, 1, this->min_length); ProtoSize::add_uint32_field(total_size, 1, this->max_length); ProtoSize::add_string_field(total_size, 1, this->pattern); ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void TextStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } void TextStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->state); ProtoSize::add_bool_field(total_size, 1, this->missing_state); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 3: { +#ifdef USE_DEVICES + case 3: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->state = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_DATETIME_DATE @@ -2824,21 +2638,27 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(8, this->device_id); +#endif } void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void DateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -2846,47 +2666,50 @@ void DateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->year); buffer.encode_uint32(4, this->month); buffer.encode_uint32(5, this->day); +#ifdef USE_DEVICES buffer.encode_uint32(6, this->device_id); +#endif } void DateStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->missing_state); ProtoSize::add_uint32_field(total_size, 1, this->year); ProtoSize::add_uint32_field(total_size, 1, this->month); ProtoSize::add_uint32_field(total_size, 1, this->day); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->year = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->month = value.as_uint32(); - return true; - } - case 4: { + break; + case 4: this->day = value.as_uint32(); - return true; - } - case 5: { + break; +#ifdef USE_DEVICES + case 5: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_DATETIME_TIME @@ -2894,21 +2717,27 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(8, this->device_id); +#endif } void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -2916,47 +2745,50 @@ void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->hour); buffer.encode_uint32(4, this->minute); buffer.encode_uint32(5, this->second); +#ifdef USE_DEVICES buffer.encode_uint32(6, this->device_id); +#endif } void TimeStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->missing_state); ProtoSize::add_uint32_field(total_size, 1, this->hour); ProtoSize::add_uint32_field(total_size, 1, this->minute); ProtoSize::add_uint32_field(total_size, 1, this->second); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->hour = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->minute = value.as_uint32(); - return true; - } - case 4: { + break; + case 4: this->second = value.as_uint32(); - return true; - } - case 5: { + break; +#ifdef USE_DEVICES + case 5: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_EVENT @@ -2964,22 +2796,26 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_string(8, this->device_class); for (auto &it : this->event_types) { buffer.encode_string(9, it, true); } +#ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); +#endif } void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_string_field(total_size, 1, this->device_class); @@ -2988,17 +2824,23 @@ void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field_repeated(total_size, 1, it); } } +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void EventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_string(2, this->event_type); +#ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); +#endif } void EventResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->event_type); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } #endif #ifdef USE_VALVE @@ -3006,73 +2848,82 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_string(8, this->device_class); buffer.encode_bool(9, this->assumed_state); buffer.encode_bool(10, this->supports_position); buffer.encode_bool(11, this->supports_stop); +#ifdef USE_DEVICES buffer.encode_uint32(12, this->device_id); +#endif } void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_string_field(total_size, 1, this->device_class); ProtoSize::add_bool_field(total_size, 1, this->assumed_state); ProtoSize::add_bool_field(total_size, 1, this->supports_position); ProtoSize::add_bool_field(total_size, 1, this->supports_stop); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void ValveStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->position); buffer.encode_uint32(3, static_cast(this->current_operation)); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } void ValveStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_float_field(total_size, 1, this->position); ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_position = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->stop = value.as_bool(); - return true; - } - case 5: { + break; +#ifdef USE_DEVICES + case 5: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 3: { + break; + case 3: this->position = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_DATETIME_DATETIME @@ -3080,57 +2931,68 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(8, this->device_id); +#endif } void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_fixed32(3, this->epoch_seconds); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } void DateTimeStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->missing_state); - ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->epoch_seconds); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool DateTimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 3: { +#ifdef USE_DEVICES + case 3: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 2: { + break; + case 2: this->epoch_seconds = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_UPDATE @@ -3138,23 +3000,29 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); +#ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_string(8, this->device_class); +#ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); +#endif } void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); +#ifdef USE_ENTITY_ICON ProtoSize::add_string_field(total_size, 1, this->icon); +#endif ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); ProtoSize::add_string_field(total_size, 1, this->device_class); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -3167,44 +3035,49 @@ void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(8, this->title); buffer.encode_string(9, this->release_summary); buffer.encode_string(10, this->release_url); +#ifdef USE_DEVICES buffer.encode_uint32(11, this->device_id); +#endif } void UpdateStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); + ProtoSize::add_fixed32_field(total_size, 1, this->key); ProtoSize::add_bool_field(total_size, 1, this->missing_state); ProtoSize::add_bool_field(total_size, 1, this->in_progress); ProtoSize::add_bool_field(total_size, 1, this->has_progress); - ProtoSize::add_fixed_field<4>(total_size, 1, this->progress != 0.0f); + ProtoSize::add_float_field(total_size, 1, this->progress); ProtoSize::add_string_field(total_size, 1, this->current_version); ProtoSize::add_string_field(total_size, 1, this->latest_version); ProtoSize::add_string_field(total_size, 1, this->title); ProtoSize::add_string_field(total_size, 1, this->release_summary); ProtoSize::add_string_field(total_size, 1, this->release_url); +#ifdef USE_DEVICES ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#endif } bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->command = static_cast(value.as_uint32()); - return true; - } - case 3: { + break; +#ifdef USE_DEVICES + case 3: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 3f2d4afad3..39f00b4adc 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -291,7 +291,6 @@ class InfoResponseProtoMessage : public ProtoMessage { std::string object_id{}; uint32_t key{0}; std::string name{}; - std::string unique_id{}; bool disabled_by_default{false}; std::string icon{}; enums::EntityCategory entity_category{}; @@ -309,7 +308,7 @@ class StateResponseProtoMessage : public ProtoMessage { protected: }; -class CommandProtoMessage : public ProtoMessage { +class CommandProtoMessage : public ProtoDecodableMessage { public: ~CommandProtoMessage() override = default; uint32_t key{0}; @@ -317,7 +316,7 @@ class CommandProtoMessage : public ProtoMessage { protected: }; -class HelloRequest : public ProtoMessage { +class HelloRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 1; static constexpr uint8_t ESTIMATED_SIZE = 17; @@ -354,7 +353,7 @@ class HelloResponse : public ProtoMessage { protected: }; -class ConnectRequest : public ProtoMessage { +class ConnectRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 3; static constexpr uint8_t ESTIMATED_SIZE = 9; @@ -385,7 +384,7 @@ class ConnectResponse : public ProtoMessage { protected: }; -class DisconnectRequest : public ProtoMessage { +class DisconnectRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 5; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -398,7 +397,7 @@ class DisconnectRequest : public ProtoMessage { protected: }; -class DisconnectResponse : public ProtoMessage { +class DisconnectResponse : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 6; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -411,7 +410,7 @@ class DisconnectResponse : public ProtoMessage { protected: }; -class PingRequest : public ProtoMessage { +class PingRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 7; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -424,7 +423,7 @@ class PingRequest : public ProtoMessage { protected: }; -class PingResponse : public ProtoMessage { +class PingResponse : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 8; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -437,7 +436,7 @@ class PingResponse : public ProtoMessage { protected: }; -class DeviceInfoRequest : public ProtoMessage { +class DeviceInfoRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 9; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -461,8 +460,6 @@ class AreaInfo : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class DeviceInfo : public ProtoMessage { public: @@ -476,8 +473,6 @@ class DeviceInfo : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class DeviceInfoResponse : public ProtoMessage { public: @@ -492,22 +487,50 @@ class DeviceInfoResponse : public ProtoMessage { std::string esphome_version{}; std::string compilation_time{}; std::string model{}; +#ifdef USE_DEEP_SLEEP bool has_deep_sleep{false}; +#endif +#ifdef ESPHOME_PROJECT_NAME std::string project_name{}; +#endif +#ifdef ESPHOME_PROJECT_NAME std::string project_version{}; +#endif +#ifdef USE_WEBSERVER uint32_t webserver_port{0}; +#endif +#ifdef USE_BLUETOOTH_PROXY uint32_t legacy_bluetooth_proxy_version{0}; +#endif +#ifdef USE_BLUETOOTH_PROXY uint32_t bluetooth_proxy_feature_flags{0}; +#endif std::string manufacturer{}; std::string friendly_name{}; +#ifdef USE_VOICE_ASSISTANT uint32_t legacy_voice_assistant_version{0}; +#endif +#ifdef USE_VOICE_ASSISTANT uint32_t voice_assistant_feature_flags{0}; +#endif +#ifdef USE_AREAS std::string suggested_area{}; +#endif +#ifdef USE_BLUETOOTH_PROXY std::string bluetooth_mac_address{}; +#endif +#ifdef USE_API_NOISE bool api_encryption_supported{false}; +#endif +#ifdef USE_DEVICES std::vector devices{}; +#endif +#ifdef USE_AREAS std::vector areas{}; +#endif +#ifdef USE_AREAS AreaInfo area{}; +#endif void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -516,7 +539,7 @@ class DeviceInfoResponse : public ProtoMessage { protected: }; -class ListEntitiesRequest : public ProtoMessage { +class ListEntitiesRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 11; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -542,7 +565,7 @@ class ListEntitiesDoneResponse : public ProtoMessage { protected: }; -class SubscribeStatesRequest : public ProtoMessage { +class SubscribeStatesRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 20; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -559,7 +582,7 @@ class SubscribeStatesRequest : public ProtoMessage { class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 12; - static constexpr uint8_t ESTIMATED_SIZE = 60; + static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_binary_sensor_response"; } #endif @@ -595,7 +618,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage { class ListEntitiesCoverResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 13; - static constexpr uint8_t ESTIMATED_SIZE = 66; + static constexpr uint8_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_cover_response"; } #endif @@ -658,7 +681,7 @@ class CoverCommandRequest : public CommandProtoMessage { class ListEntitiesFanResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 14; - static constexpr uint8_t ESTIMATED_SIZE = 77; + static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_fan_response"; } #endif @@ -729,7 +752,7 @@ class FanCommandRequest : public CommandProtoMessage { class ListEntitiesLightResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 15; - static constexpr uint8_t ESTIMATED_SIZE = 90; + static constexpr uint8_t ESTIMATED_SIZE = 81; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_light_response"; } #endif @@ -823,7 +846,7 @@ class LightCommandRequest : public CommandProtoMessage { class ListEntitiesSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 16; - static constexpr uint8_t ESTIMATED_SIZE = 77; + static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_sensor_response"; } #endif @@ -863,7 +886,7 @@ class SensorStateResponse : public StateResponseProtoMessage { class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 17; - static constexpr uint8_t ESTIMATED_SIZE = 60; + static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_switch_response"; } #endif @@ -914,7 +937,7 @@ class SwitchCommandRequest : public CommandProtoMessage { class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 18; - static constexpr uint8_t ESTIMATED_SIZE = 58; + static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif @@ -945,7 +968,7 @@ class TextSensorStateResponse : public StateResponseProtoMessage { protected: }; #endif -class SubscribeLogsRequest : public ProtoMessage { +class SubscribeLogsRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 28; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -980,7 +1003,7 @@ class SubscribeLogsResponse : public ProtoMessage { protected: }; #ifdef USE_API_NOISE -class NoiseEncryptionSetKeyRequest : public ProtoMessage { +class NoiseEncryptionSetKeyRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 124; static constexpr uint8_t ESTIMATED_SIZE = 9; @@ -1012,7 +1035,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { protected: }; #endif -class SubscribeHomeassistantServicesRequest : public ProtoMessage { +class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 34; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -1036,7 +1059,6 @@ class HomeassistantServiceMap : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; class HomeassistantServiceResponse : public ProtoMessage { public: @@ -1058,7 +1080,7 @@ class HomeassistantServiceResponse : public ProtoMessage { protected: }; -class SubscribeHomeAssistantStatesRequest : public ProtoMessage { +class SubscribeHomeAssistantStatesRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 38; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -1089,7 +1111,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { protected: }; -class HomeAssistantStateResponse : public ProtoMessage { +class HomeAssistantStateResponse : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 40; static constexpr uint8_t ESTIMATED_SIZE = 27; @@ -1106,7 +1128,7 @@ class HomeAssistantStateResponse : public ProtoMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class GetTimeRequest : public ProtoMessage { +class GetTimeRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 36; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -1119,7 +1141,7 @@ class GetTimeRequest : public ProtoMessage { protected: }; -class GetTimeResponse : public ProtoMessage { +class GetTimeResponse : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; static constexpr uint8_t ESTIMATED_SIZE = 5; @@ -1148,8 +1170,6 @@ class ListEntitiesServicesArgument : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class ListEntitiesServicesResponse : public ProtoMessage { public: @@ -1169,7 +1189,7 @@ class ListEntitiesServicesResponse : public ProtoMessage { protected: }; -class ExecuteServiceArgument : public ProtoMessage { +class ExecuteServiceArgument : public ProtoDecodableMessage { public: bool bool_{false}; int32_t legacy_int{0}; @@ -1180,8 +1200,6 @@ class ExecuteServiceArgument : public ProtoMessage { std::vector int_array{}; std::vector float_array{}; std::vector string_array{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1191,7 +1209,7 @@ class ExecuteServiceArgument : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ExecuteServiceRequest : public ProtoMessage { +class ExecuteServiceRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 42; static constexpr uint8_t ESTIMATED_SIZE = 39; @@ -1213,7 +1231,7 @@ class ExecuteServiceRequest : public ProtoMessage { class ListEntitiesCameraResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 43; - static constexpr uint8_t ESTIMATED_SIZE = 49; + static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_camera_response"; } #endif @@ -1242,7 +1260,7 @@ class CameraImageResponse : public StateResponseProtoMessage { protected: }; -class CameraImageRequest : public ProtoMessage { +class CameraImageRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 45; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1263,7 +1281,7 @@ class CameraImageRequest : public ProtoMessage { class ListEntitiesClimateResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 46; - static constexpr uint8_t ESTIMATED_SIZE = 156; + static constexpr uint8_t ESTIMATED_SIZE = 147; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_climate_response"; } #endif @@ -1365,7 +1383,7 @@ class ClimateCommandRequest : public CommandProtoMessage { class ListEntitiesNumberResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 49; - static constexpr uint8_t ESTIMATED_SIZE = 84; + static constexpr uint8_t ESTIMATED_SIZE = 75; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_number_response"; } #endif @@ -1421,7 +1439,7 @@ class NumberCommandRequest : public CommandProtoMessage { class ListEntitiesSelectResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 52; - static constexpr uint8_t ESTIMATED_SIZE = 67; + static constexpr uint8_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_select_response"; } #endif @@ -1473,7 +1491,7 @@ class SelectCommandRequest : public CommandProtoMessage { class ListEntitiesSirenResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 55; - static constexpr uint8_t ESTIMATED_SIZE = 71; + static constexpr uint8_t ESTIMATED_SIZE = 62; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_siren_response"; } #endif @@ -1533,7 +1551,7 @@ class SirenCommandRequest : public CommandProtoMessage { class ListEntitiesLockResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 58; - static constexpr uint8_t ESTIMATED_SIZE = 64; + static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_lock_response"; } #endif @@ -1589,7 +1607,7 @@ class LockCommandRequest : public CommandProtoMessage { class ListEntitiesButtonResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 61; - static constexpr uint8_t ESTIMATED_SIZE = 58; + static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_button_response"; } #endif @@ -1633,13 +1651,11 @@ class MediaPlayerSupportedFormat : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 63; - static constexpr uint8_t ESTIMATED_SIZE = 85; + static constexpr uint8_t ESTIMATED_SIZE = 76; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_media_player_response"; } #endif @@ -1697,7 +1713,7 @@ class MediaPlayerCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_BLUETOOTH_PROXY -class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { +class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 66; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1724,8 +1740,6 @@ class BluetoothServiceData : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothLEAdvertisementResponse : public ProtoMessage { public: @@ -1754,7 +1768,8 @@ class BluetoothLERawAdvertisement : public ProtoMessage { uint64_t address{0}; int32_t rssi{0}; uint32_t address_type{0}; - std::string data{}; + uint8_t data[62]{}; + uint8_t data_len{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1762,8 +1777,6 @@ class BluetoothLERawAdvertisement : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothLERawAdvertisementsResponse : public ProtoMessage { public: @@ -1781,7 +1794,7 @@ class BluetoothLERawAdvertisementsResponse : public ProtoMessage { protected: }; -class BluetoothDeviceRequest : public ProtoMessage { +class BluetoothDeviceRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 68; static constexpr uint8_t ESTIMATED_SIZE = 12; @@ -1818,7 +1831,7 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage { protected: }; -class BluetoothGATTGetServicesRequest : public ProtoMessage { +class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 70; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1844,7 +1857,6 @@ class BluetoothGATTDescriptor : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothGATTCharacteristic : public ProtoMessage { public: @@ -1859,8 +1871,6 @@ class BluetoothGATTCharacteristic : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothGATTService : public ProtoMessage { public: @@ -1874,8 +1884,6 @@ class BluetoothGATTService : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothGATTGetServicesResponse : public ProtoMessage { public: @@ -1910,7 +1918,7 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage { protected: }; -class BluetoothGATTReadRequest : public ProtoMessage { +class BluetoothGATTReadRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 73; static constexpr uint8_t ESTIMATED_SIZE = 8; @@ -1944,7 +1952,7 @@ class BluetoothGATTReadResponse : public ProtoMessage { protected: }; -class BluetoothGATTWriteRequest : public ProtoMessage { +class BluetoothGATTWriteRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 75; static constexpr uint8_t ESTIMATED_SIZE = 19; @@ -1963,7 +1971,7 @@ class BluetoothGATTWriteRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTReadDescriptorRequest : public ProtoMessage { +class BluetoothGATTReadDescriptorRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 76; static constexpr uint8_t ESTIMATED_SIZE = 8; @@ -1979,7 +1987,7 @@ class BluetoothGATTReadDescriptorRequest : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTWriteDescriptorRequest : public ProtoMessage { +class BluetoothGATTWriteDescriptorRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 77; static constexpr uint8_t ESTIMATED_SIZE = 17; @@ -1997,7 +2005,7 @@ class BluetoothGATTWriteDescriptorRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTNotifyRequest : public ProtoMessage { +class BluetoothGATTNotifyRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 78; static constexpr uint8_t ESTIMATED_SIZE = 10; @@ -2032,7 +2040,7 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage { protected: }; -class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { +class SubscribeBluetoothConnectionsFreeRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 80; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -2151,7 +2159,7 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage { protected: }; -class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { +class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 87; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -2199,7 +2207,7 @@ class BluetoothScannerStateResponse : public ProtoMessage { protected: }; -class BluetoothScannerSetModeRequest : public ProtoMessage { +class BluetoothScannerSetModeRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 127; static constexpr uint8_t ESTIMATED_SIZE = 2; @@ -2216,7 +2224,7 @@ class BluetoothScannerSetModeRequest : public ProtoMessage { }; #endif #ifdef USE_VOICE_ASSISTANT -class SubscribeVoiceAssistantRequest : public ProtoMessage { +class SubscribeVoiceAssistantRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 89; static constexpr uint8_t ESTIMATED_SIZE = 6; @@ -2244,8 +2252,6 @@ class VoiceAssistantAudioSettings : public ProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class VoiceAssistantRequest : public ProtoMessage { public: @@ -2267,7 +2273,7 @@ class VoiceAssistantRequest : public ProtoMessage { protected: }; -class VoiceAssistantResponse : public ProtoMessage { +class VoiceAssistantResponse : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 91; static constexpr uint8_t ESTIMATED_SIZE = 6; @@ -2283,12 +2289,10 @@ class VoiceAssistantResponse : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantEventData : public ProtoMessage { +class VoiceAssistantEventData : public ProtoDecodableMessage { public: std::string name{}; std::string value{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2296,7 +2300,7 @@ class VoiceAssistantEventData : public ProtoMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class VoiceAssistantEventResponse : public ProtoMessage { +class VoiceAssistantEventResponse : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 92; static constexpr uint8_t ESTIMATED_SIZE = 36; @@ -2313,7 +2317,7 @@ class VoiceAssistantEventResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantAudio : public ProtoMessage { +class VoiceAssistantAudio : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 106; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -2332,7 +2336,7 @@ class VoiceAssistantAudio : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantTimerEventResponse : public ProtoMessage { +class VoiceAssistantTimerEventResponse : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 115; static constexpr uint8_t ESTIMATED_SIZE = 30; @@ -2353,7 +2357,7 @@ class VoiceAssistantTimerEventResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantAnnounceRequest : public ProtoMessage { +class VoiceAssistantAnnounceRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 119; static constexpr uint8_t ESTIMATED_SIZE = 29; @@ -2400,9 +2404,8 @@ class VoiceAssistantWakeWord : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class VoiceAssistantConfigurationRequest : public ProtoMessage { +class VoiceAssistantConfigurationRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 121; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -2433,7 +2436,7 @@ class VoiceAssistantConfigurationResponse : public ProtoMessage { protected: }; -class VoiceAssistantSetConfiguration : public ProtoMessage { +class VoiceAssistantSetConfiguration : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 123; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -2453,7 +2456,7 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 94; - static constexpr uint8_t ESTIMATED_SIZE = 57; + static constexpr uint8_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_alarm_control_panel_response"; } #endif @@ -2507,7 +2510,7 @@ class AlarmControlPanelCommandRequest : public CommandProtoMessage { class ListEntitiesTextResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 97; - static constexpr uint8_t ESTIMATED_SIZE = 68; + static constexpr uint8_t ESTIMATED_SIZE = 59; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_text_response"; } #endif @@ -2562,7 +2565,7 @@ class TextCommandRequest : public CommandProtoMessage { class ListEntitiesDateResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 100; - static constexpr uint8_t ESTIMATED_SIZE = 49; + static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_response"; } #endif @@ -2616,7 +2619,7 @@ class DateCommandRequest : public CommandProtoMessage { class ListEntitiesTimeResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 103; - static constexpr uint8_t ESTIMATED_SIZE = 49; + static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_time_response"; } #endif @@ -2670,7 +2673,7 @@ class TimeCommandRequest : public CommandProtoMessage { class ListEntitiesEventResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 107; - static constexpr uint8_t ESTIMATED_SIZE = 76; + static constexpr uint8_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_event_response"; } #endif @@ -2705,7 +2708,7 @@ class EventResponse : public StateResponseProtoMessage { class ListEntitiesValveResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 109; - static constexpr uint8_t ESTIMATED_SIZE = 64; + static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_valve_response"; } #endif @@ -2761,7 +2764,7 @@ class ValveCommandRequest : public CommandProtoMessage { class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 112; - static constexpr uint8_t ESTIMATED_SIZE = 49; + static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_time_response"; } #endif @@ -2811,7 +2814,7 @@ class DateTimeCommandRequest : public CommandProtoMessage { class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 116; - static constexpr uint8_t ESTIMATED_SIZE = 58; + static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_update_response"; } #endif diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index f6509f47cc..ad5a5fdcaa 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -713,33 +713,45 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("'").append(this->model).append("'"); out.append("\n"); +#ifdef USE_DEEP_SLEEP out.append(" has_deep_sleep: "); out.append(YESNO(this->has_deep_sleep)); out.append("\n"); +#endif +#ifdef ESPHOME_PROJECT_NAME out.append(" project_name: "); out.append("'").append(this->project_name).append("'"); out.append("\n"); +#endif +#ifdef ESPHOME_PROJECT_NAME out.append(" project_version: "); out.append("'").append(this->project_version).append("'"); out.append("\n"); +#endif +#ifdef USE_WEBSERVER out.append(" webserver_port: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->webserver_port); out.append(buffer); out.append("\n"); +#endif +#ifdef USE_BLUETOOTH_PROXY out.append(" legacy_bluetooth_proxy_version: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_bluetooth_proxy_version); out.append(buffer); out.append("\n"); +#endif +#ifdef USE_BLUETOOTH_PROXY out.append(" bluetooth_proxy_feature_flags: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->bluetooth_proxy_feature_flags); out.append(buffer); out.append("\n"); +#endif out.append(" manufacturer: "); out.append("'").append(this->manufacturer).append("'"); out.append("\n"); @@ -748,43 +760,60 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("'").append(this->friendly_name).append("'"); out.append("\n"); +#ifdef USE_VOICE_ASSISTANT out.append(" legacy_voice_assistant_version: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_voice_assistant_version); out.append(buffer); out.append("\n"); +#endif +#ifdef USE_VOICE_ASSISTANT out.append(" voice_assistant_feature_flags: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->voice_assistant_feature_flags); out.append(buffer); out.append("\n"); +#endif +#ifdef USE_AREAS out.append(" suggested_area: "); out.append("'").append(this->suggested_area).append("'"); out.append("\n"); +#endif +#ifdef USE_BLUETOOTH_PROXY out.append(" bluetooth_mac_address: "); out.append("'").append(this->bluetooth_mac_address).append("'"); out.append("\n"); +#endif +#ifdef USE_API_NOISE out.append(" api_encryption_supported: "); out.append(YESNO(this->api_encryption_supported)); out.append("\n"); +#endif +#ifdef USE_DEVICES for (const auto &it : this->devices) { out.append(" devices: "); it.dump_to(out); out.append("\n"); } +#endif +#ifdef USE_AREAS for (const auto &it : this->areas) { out.append(" areas: "); it.dump_to(out); out.append("\n"); } +#endif +#ifdef USE_AREAS out.append(" area: "); this->area.dump_to(out); out.append("\n"); + +#endif out.append("}"); } void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } @@ -807,10 +836,6 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - out.append(" device_class: "); out.append("'").append(this->device_class).append("'"); out.append("\n"); @@ -823,18 +848,23 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { out.append(YESNO(this->disabled_by_default)); out.append("\n"); +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void BinarySensorStateResponse::dump_to(std::string &out) const { @@ -853,10 +883,13 @@ void BinarySensorStateResponse::dump_to(std::string &out) const { out.append(YESNO(this->missing_state)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -877,10 +910,6 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - out.append(" assumed_state: "); out.append(YESNO(this->assumed_state)); out.append("\n"); @@ -901,10 +930,12 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append(YESNO(this->disabled_by_default)); out.append("\n"); +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); @@ -913,10 +944,13 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { out.append(YESNO(this->supports_stop)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void CoverStateResponse::dump_to(std::string &out) const { @@ -945,10 +979,13 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->current_operation)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void CoverCommandRequest::dump_to(std::string &out) const { @@ -989,10 +1026,13 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append(YESNO(this->stop)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -1013,10 +1053,6 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - out.append(" supports_oscillation: "); out.append(YESNO(this->supports_oscillation)); out.append("\n"); @@ -1038,10 +1074,12 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append(YESNO(this->disabled_by_default)); out.append("\n"); +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); @@ -1052,10 +1090,13 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append("\n"); } +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void FanStateResponse::dump_to(std::string &out) const { @@ -1091,10 +1132,13 @@ void FanStateResponse::dump_to(std::string &out) const { out.append("'").append(this->preset_mode).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void FanCommandRequest::dump_to(std::string &out) const { @@ -1154,10 +1198,13 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append("'").append(this->preset_mode).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -1178,10 +1225,6 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - for (const auto &it : this->supported_color_modes) { out.append(" supported_color_modes: "); out.append(proto_enum_to_string(it)); @@ -1224,18 +1267,23 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append(YESNO(this->disabled_by_default)); out.append("\n"); +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void LightStateResponse::dump_to(std::string &out) const { @@ -1303,10 +1351,13 @@ void LightStateResponse::dump_to(std::string &out) const { out.append("'").append(this->effect).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void LightCommandRequest::dump_to(std::string &out) const { @@ -1432,10 +1483,13 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append("'").append(this->effect).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -1456,14 +1510,12 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" unit_of_measurement: "); out.append("'").append(this->unit_of_measurement).append("'"); out.append("\n"); @@ -1497,10 +1549,13 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void SensorStateResponse::dump_to(std::string &out) const { @@ -1520,10 +1575,13 @@ void SensorStateResponse::dump_to(std::string &out) const { out.append(YESNO(this->missing_state)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -1544,14 +1602,12 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" assumed_state: "); out.append(YESNO(this->assumed_state)); out.append("\n"); @@ -1568,10 +1624,13 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append("'").append(this->device_class).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void SwitchStateResponse::dump_to(std::string &out) const { @@ -1586,10 +1645,13 @@ void SwitchStateResponse::dump_to(std::string &out) const { out.append(YESNO(this->state)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void SwitchCommandRequest::dump_to(std::string &out) const { @@ -1604,10 +1666,13 @@ void SwitchCommandRequest::dump_to(std::string &out) const { out.append(YESNO(this->state)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -1628,14 +1693,12 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -1648,10 +1711,13 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { out.append("'").append(this->device_class).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void TextSensorStateResponse::dump_to(std::string &out) const { @@ -1670,10 +1736,13 @@ void TextSensorStateResponse::dump_to(std::string &out) const { out.append(YESNO(this->missing_state)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -1931,26 +2000,27 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void CameraImageResponse::dump_to(std::string &out) const { @@ -1969,10 +2039,13 @@ void CameraImageResponse::dump_to(std::string &out) const { out.append(YESNO(this->done)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void CameraImageRequest::dump_to(std::string &out) const { @@ -2005,10 +2078,6 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - out.append(" supports_current_temperature: "); out.append(YESNO(this->supports_current_temperature)); out.append("\n"); @@ -2080,10 +2149,12 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(YESNO(this->disabled_by_default)); out.append("\n"); +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); @@ -2111,10 +2182,13 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void ClimateStateResponse::dump_to(std::string &out) const { @@ -2187,10 +2261,13 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void ClimateCommandRequest::dump_to(std::string &out) const { @@ -2293,10 +2370,13 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -2317,14 +2397,12 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" min_value: "); snprintf(buffer, sizeof(buffer), "%g", this->min_value); out.append(buffer); @@ -2360,10 +2438,13 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const { out.append("'").append(this->device_class).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void NumberStateResponse::dump_to(std::string &out) const { @@ -2383,10 +2464,13 @@ void NumberStateResponse::dump_to(std::string &out) const { out.append(YESNO(this->missing_state)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void NumberCommandRequest::dump_to(std::string &out) const { @@ -2402,10 +2486,13 @@ void NumberCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -2426,14 +2513,12 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif for (const auto &it : this->options) { out.append(" options: "); out.append("'").append(it).append("'"); @@ -2448,10 +2533,13 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void SelectStateResponse::dump_to(std::string &out) const { @@ -2470,10 +2558,13 @@ void SelectStateResponse::dump_to(std::string &out) const { out.append(YESNO(this->missing_state)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void SelectCommandRequest::dump_to(std::string &out) const { @@ -2488,10 +2579,13 @@ void SelectCommandRequest::dump_to(std::string &out) const { out.append("'").append(this->state).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -2512,14 +2606,12 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -2542,10 +2634,13 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void SirenStateResponse::dump_to(std::string &out) const { @@ -2560,10 +2655,13 @@ void SirenStateResponse::dump_to(std::string &out) const { out.append(YESNO(this->state)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void SirenCommandRequest::dump_to(std::string &out) const { @@ -2608,10 +2706,13 @@ void SirenCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -2632,14 +2733,12 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -2664,10 +2763,13 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const { out.append("'").append(this->code_format).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void LockStateResponse::dump_to(std::string &out) const { @@ -2682,10 +2784,13 @@ void LockStateResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->state)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void LockCommandRequest::dump_to(std::string &out) const { @@ -2708,10 +2813,13 @@ void LockCommandRequest::dump_to(std::string &out) const { out.append("'").append(this->code).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -2732,14 +2840,12 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -2752,10 +2858,13 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const { out.append("'").append(this->device_class).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void ButtonCommandRequest::dump_to(std::string &out) const { @@ -2766,10 +2875,13 @@ void ButtonCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -2817,14 +2929,12 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -2843,10 +2953,13 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { out.append("\n"); } +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void MediaPlayerStateResponse::dump_to(std::string &out) const { @@ -2870,10 +2983,13 @@ void MediaPlayerStateResponse::dump_to(std::string &out) const { out.append(YESNO(this->muted)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void MediaPlayerCommandRequest::dump_to(std::string &out) const { @@ -2917,10 +3033,13 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { out.append(YESNO(this->announcement)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -3013,7 +3132,7 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const { out.append("\n"); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + out.append(format_hex_pretty(this->data, this->data_len)); out.append("\n"); out.append("}"); } @@ -3678,14 +3797,12 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -3707,10 +3824,13 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { out.append(YESNO(this->requires_code_to_arm)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void AlarmControlPanelStateResponse::dump_to(std::string &out) const { @@ -3725,10 +3845,13 @@ void AlarmControlPanelStateResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->state)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { @@ -3747,10 +3870,13 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { out.append("'").append(this->code).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -3771,14 +3897,12 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -3805,10 +3929,13 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->mode)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void TextStateResponse::dump_to(std::string &out) const { @@ -3827,10 +3954,13 @@ void TextStateResponse::dump_to(std::string &out) const { out.append(YESNO(this->missing_state)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void TextCommandRequest::dump_to(std::string &out) const { @@ -3845,10 +3975,13 @@ void TextCommandRequest::dump_to(std::string &out) const { out.append("'").append(this->state).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -3869,14 +4002,12 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -3885,10 +4016,13 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void DateStateResponse::dump_to(std::string &out) const { @@ -3918,10 +4052,13 @@ void DateStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void DateCommandRequest::dump_to(std::string &out) const { @@ -3947,10 +4084,13 @@ void DateCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -3971,14 +4111,12 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -3987,10 +4125,13 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void TimeStateResponse::dump_to(std::string &out) const { @@ -4020,10 +4161,13 @@ void TimeStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void TimeCommandRequest::dump_to(std::string &out) const { @@ -4049,10 +4193,13 @@ void TimeCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -4073,14 +4220,12 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -4099,10 +4244,13 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { out.append("\n"); } +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void EventResponse::dump_to(std::string &out) const { @@ -4117,10 +4265,13 @@ void EventResponse::dump_to(std::string &out) const { out.append("'").append(this->event_type).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -4141,14 +4292,12 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -4173,10 +4322,13 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const { out.append(YESNO(this->supports_stop)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void ValveStateResponse::dump_to(std::string &out) const { @@ -4196,10 +4348,13 @@ void ValveStateResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->current_operation)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void ValveCommandRequest::dump_to(std::string &out) const { @@ -4223,10 +4378,13 @@ void ValveCommandRequest::dump_to(std::string &out) const { out.append(YESNO(this->stop)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -4247,14 +4405,12 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -4263,10 +4419,13 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void DateTimeStateResponse::dump_to(std::string &out) const { @@ -4286,10 +4445,13 @@ void DateTimeStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void DateTimeCommandRequest::dump_to(std::string &out) const { @@ -4305,10 +4467,13 @@ void DateTimeCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif @@ -4329,14 +4494,12 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const { out.append("'").append(this->name).append("'"); out.append("\n"); - out.append(" unique_id: "); - out.append("'").append(this->unique_id).append("'"); - out.append("\n"); - +#ifdef USE_ENTITY_ICON out.append(" icon: "); out.append("'").append(this->icon).append("'"); out.append("\n"); +#endif out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -4349,10 +4512,13 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const { out.append("'").append(this->device_class).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void UpdateStateResponse::dump_to(std::string &out) const { @@ -4400,10 +4566,13 @@ void UpdateStateResponse::dump_to(std::string &out) const { out.append("'").append(this->release_url).append("'"); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } void UpdateCommandRequest::dump_to(std::string &out) const { @@ -4418,10 +4587,13 @@ void UpdateCommandRequest::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->command)); out.append("\n"); +#ifdef USE_DEVICES out.append(" device_id: "); snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); out.append(buffer); out.append("\n"); + +#endif out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index b96e5736a4..888dc16836 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -598,32 +598,32 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, void APIServerConnection::on_hello_request(const HelloRequest &msg) { HelloResponse ret = this->hello(msg); - if (!this->send_message(ret)) { + if (!this->send_message(ret, HelloResponse::MESSAGE_TYPE)) { this->on_fatal_error(); } } void APIServerConnection::on_connect_request(const ConnectRequest &msg) { ConnectResponse ret = this->connect(msg); - if (!this->send_message(ret)) { + if (!this->send_message(ret, ConnectResponse::MESSAGE_TYPE)) { this->on_fatal_error(); } } void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { DisconnectResponse ret = this->disconnect(msg); - if (!this->send_message(ret)) { + if (!this->send_message(ret, DisconnectResponse::MESSAGE_TYPE)) { this->on_fatal_error(); } } void APIServerConnection::on_ping_request(const PingRequest &msg) { PingResponse ret = this->ping(msg); - if (!this->send_message(ret)) { + if (!this->send_message(ret, PingResponse::MESSAGE_TYPE)) { this->on_fatal_error(); } } void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { if (this->check_connection_setup_()) { DeviceInfoResponse ret = this->device_info(msg); - if (!this->send_message(ret)) { + if (!this->send_message(ret, DeviceInfoResponse::MESSAGE_TYPE)) { this->on_fatal_error(); } } @@ -657,7 +657,7 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { if (this->check_connection_setup_()) { GetTimeResponse ret = this->get_time(msg); - if (!this->send_message(ret)) { + if (!this->send_message(ret, GetTimeResponse::MESSAGE_TYPE)) { this->on_fatal_error(); } } @@ -673,7 +673,7 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { if (this->check_authenticated_()) { NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); - if (!this->send_message(ret)) { + if (!this->send_message(ret, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE)) { this->on_fatal_error(); } } @@ -867,7 +867,7 @@ void APIServerConnection::on_subscribe_bluetooth_connections_free_request( const SubscribeBluetoothConnectionsFreeRequest &msg) { if (this->check_authenticated_()) { BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); - if (!this->send_message(ret)) { + if (!this->send_message(ret, BluetoothConnectionsFreeResponse::MESSAGE_TYPE)) { this->on_fatal_error(); } } @@ -899,7 +899,7 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { if (this->check_authenticated_()) { VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); - if (!this->send_message(ret)) { + if (!this->send_message(ret, VoiceAssistantConfigurationResponse::MESSAGE_TYPE)) { this->on_fatal_error(); } } diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 9c5dc244fe..f7076a28ca 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -18,11 +18,11 @@ class APIServerConnectionBase : public ProtoService { public: #endif - template bool send_message(const T &msg) { + bool send_message(const ProtoMessage &msg, uint8_t message_type) { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_send_message_(msg.message_name(), msg.dump()); #endif - return this->send_message_(msg, T::MESSAGE_TYPE); + return this->send_message_(msg, message_type); } virtual void on_hello_request(const HelloRequest &value){}; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index f5be672c9a..78c04f79c2 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -31,7 +31,6 @@ APIServer::APIServer() { } void APIServer::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->setup_controller(); #ifdef USE_API_NOISE @@ -105,7 +104,7 @@ void APIServer::setup() { return; } for (auto &c : this->clients_) { - if (!c->flags_.remove) + if (!c->flags_.remove && c->get_log_subscription_level() >= level) c->try_send_log_message(level, tag, message, message_len); } }); @@ -205,22 +204,20 @@ void APIServer::loop() { void APIServer::dump_config() { ESP_LOGCONFIG(TAG, - "API Server:\n" + "Server:\n" " Address: %s:%u", network::get_use_address().c_str(), this->port_); #ifdef USE_API_NOISE - ESP_LOGCONFIG(TAG, " Using noise encryption: %s", YESNO(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 noise encryption: YES"); + ESP_LOGCONFIG(TAG, " Supports encryption: YES"); } #else - ESP_LOGCONFIG(TAG, " Using noise encryption: NO"); + ESP_LOGCONFIG(TAG, " Noise encryption: NO"); #endif } #ifdef USE_API_PASSWORD -bool APIServer::uses_password() const { return !this->password_.empty(); } - bool APIServer::check_password(const std::string &password) const { // depend only on input password length const char *a = this->password_.c_str(); @@ -428,10 +425,11 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { ESP_LOGD(TAG, "Noise PSK saved"); if (make_active) { this->set_timeout(100, [this, psk]() { - ESP_LOGW(TAG, "Disconnecting all clients to reset connections"); + ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); this->set_noise_psk(psk); for (auto &c : this->clients_) { - c->send_message(DisconnectRequest()); + DisconnectRequest req; + c->send_message(req, DisconnectRequest::MESSAGE_TYPE); } }); } @@ -464,7 +462,8 @@ void APIServer::on_shutdown() { // Send disconnect requests to all connected clients for (auto &c : this->clients_) { - if (!c->send_message(DisconnectRequest())) { + DisconnectRequest req; + if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) { // If we can't send the disconnect request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE, diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index f41064b62b..edbd289421 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -39,7 +39,6 @@ class APIServer : public Component, public Controller { bool teardown() override; #ifdef USE_API_PASSWORD bool check_password(const std::string &password) const; - bool uses_password() const; void set_password(const std::string &password); #endif void set_port(uint16_t port); diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 32d13b69ae..f765f1f806 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -11,6 +11,18 @@ namespace esphome { namespace api { template class TemplatableStringValue : public TemplatableValue { + private: + // Helper to convert value to string - handles the case where value is already a string + template static std::string value_to_string(T &&val) { return to_string(std::forward(val)); } + + // Overloads for string types - needed because std::to_string doesn't support them + static std::string value_to_string(char *val) { + return val ? std::string(val) : std::string(); + } // For lambdas returning char* (e.g., itoa) + static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str() + static std::string value_to_string(const std::string &val) { return val; } + static std::string value_to_string(std::string &&val) { return std::move(val); } + public: TemplatableStringValue() : TemplatableValue() {} @@ -19,7 +31,7 @@ template class TemplatableStringValue : public TemplatableValue::value, int> = 0> TemplatableStringValue(F f) - : TemplatableValue([f](X... x) -> std::string { return to_string(f(x...)); }) {} + : TemplatableValue([f](X... x) -> std::string { return value_to_string(f(x...)); }) {} }; template class TemplatableKeyValuePair { diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 1fbe68117b..809c658803 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -86,7 +86,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie #ifdef USE_API_SERVICES bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); - return this->client_->send_message(resp); + return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); } #endif diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 25daf17ccc..bf64d5f723 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -8,7 +8,7 @@ namespace api { static const char *const TAG = "api.proto"; -void ProtoMessage::decode(const uint8_t *buffer, size_t length) { +void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { uint32_t i = 0; bool error = false; while (i < length) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index a435168821..a2c31100bf 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -135,6 +135,7 @@ class ProtoVarInt { // Forward declaration for decode_to_message and encode_to_writer class ProtoMessage; +class ProtoDecodableMessage; class ProtoLengthDelimited { public: @@ -142,15 +143,15 @@ class ProtoLengthDelimited { std::string as_string() const { return std::string(reinterpret_cast(this->value_), this->length_); } /** - * Decode the length-delimited data into an existing ProtoMessage instance. + * Decode the length-delimited data into an existing ProtoDecodableMessage instance. * * This method allows decoding without templates, enabling use in contexts - * where the message type is not known at compile time. The ProtoMessage's + * where the message type is not known at compile time. The ProtoDecodableMessage's * decode() method will be called with the raw data and length. * - * @param msg The ProtoMessage instance to decode into + * @param msg The ProtoDecodableMessage instance to decode into */ - void decode_to_message(ProtoMessage &msg) const; + void decode_to_message(ProtoDecodableMessage &msg) const; protected: const uint8_t *const value_; @@ -175,23 +176,7 @@ class Proto32Bit { const uint32_t value_; }; -class Proto64Bit { - public: - explicit Proto64Bit(uint64_t value) : value_(value) {} - uint64_t as_fixed64() const { return this->value_; } - int64_t as_sfixed64() const { return static_cast(this->value_); } - double as_double() const { - union { - uint64_t raw; - double value; - } s{}; - s.raw = this->value_; - return s.value; - } - - protected: - const uint64_t value_; -}; +// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported class ProtoWriteBuffer { public: @@ -205,9 +190,9 @@ class ProtoWriteBuffer { * @param field_id Field number (tag) in the protobuf message * @param type Wire type value: * - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum) - * - 1: 64-bit (fixed64, sfixed64, double) * - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields) * - 5: 32-bit (fixed32, sfixed32, float) + * - Note: Wire type 1 (64-bit fixed) is not supported * * Following https://protobuf.dev/programming-guides/encoding/#structure */ @@ -258,20 +243,10 @@ class ProtoWriteBuffer { this->write((value >> 16) & 0xFF); this->write((value >> 24) & 0xFF); } - void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) { - if (value == 0 && !force) - return; - - this->encode_field_raw(field_id, 1); // type 1: 64-bit fixed64 - this->write((value >> 0) & 0xFF); - this->write((value >> 8) & 0xFF); - this->write((value >> 16) & 0xFF); - this->write((value >> 24) & 0xFF); - this->write((value >> 32) & 0xFF); - this->write((value >> 40) & 0xFF); - this->write((value >> 48) & 0xFF); - this->write((value >> 56) & 0xFF); - } + // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally + // not supported to reduce overhead on embedded systems. All ESPHome devices are + // 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support + // is needed in the future, the necessary encoding/decoding functions must be added. void encode_float(uint32_t field_id, float value, bool force = false) { if (value == 0.0f && !force) return; @@ -324,7 +299,6 @@ class ProtoMessage { virtual ~ProtoMessage() = default; // Default implementation for messages with no fields virtual void encode(ProtoWriteBuffer buffer) const {} - void decode(const uint8_t *buffer, size_t length); // Default implementation for messages with no fields virtual void calculate_size(uint32_t &total_size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP @@ -332,12 +306,18 @@ class ProtoMessage { virtual void dump_to(std::string &out) const = 0; virtual const char *message_name() const { return "unknown"; } #endif +}; + +// Base class for messages that support decoding +class ProtoDecodableMessage : public ProtoMessage { + public: + void decode(const uint8_t *buffer, size_t length); protected: virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; } virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } - virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; } + // NOTE: decode_64bit removed - wire type 1 not supported }; class ProtoSize { @@ -566,6 +546,42 @@ class ProtoSize { total_size += field_id_size + NumBytes; } + /** + * @brief Calculates and adds the size of a float field to the total message size + */ + static inline void add_float_field(uint32_t &total_size, uint32_t field_id_size, float value) { + if (value != 0.0f) { + total_size += field_id_size + 4; + } + } + + // NOTE: add_double_field removed - wire type 1 (64-bit: double) not supported + // to reduce overhead on embedded systems + + /** + * @brief Calculates and adds the size of a fixed32 field to the total message size + */ + static inline void add_fixed32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { + if (value != 0) { + total_size += field_id_size + 4; + } + } + + // NOTE: add_fixed64_field removed - wire type 1 (64-bit: fixed64) not supported + // to reduce overhead on embedded systems + + /** + * @brief Calculates and adds the size of a sfixed32 field to the total message size + */ + static inline void add_sfixed32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) { + if (value != 0) { + total_size += field_id_size + 4; + } + } + + // NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported + // to reduce overhead on embedded systems + /** * @brief Calculates and adds the size of an enum field to the total message size * @@ -662,33 +678,8 @@ class ProtoSize { total_size += field_id_size + varint(value); } - /** - * @brief Calculates and adds the size of a sint64 field to the total message size - * - * Sint64 fields use ZigZag encoding, which is more efficient for negative values. - */ - static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) { - // Skip calculation if value is zero - if (value == 0) { - return; // No need to update total_size - } - - // ZigZag encoding for sint64: (n << 1) ^ (n >> 63) - uint64_t zigzag = (static_cast(value) << 1) ^ (static_cast(value >> 63)); - total_size += field_id_size + varint(zigzag); - } - - /** - * @brief Calculates and adds the size of a sint64 field to the total message size (repeated field version) - * - * Sint64 fields use ZigZag encoding, which is more efficient for negative values. - */ - static inline void add_sint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) { - // Always calculate size for repeated fields - // ZigZag encoding for sint64: (n << 1) ^ (n >> 63) - uint64_t zigzag = (static_cast(value) << 1) ^ (static_cast(value >> 63)); - total_size += field_id_size + varint(zigzag); - } + // NOTE: sint64 support functions (add_sint64_field, add_sint64_field_repeated) removed + // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems /** * @brief Calculates and adds the size of a string/bytes field to the total message size @@ -823,8 +814,8 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); } -// Implementation of decode_to_message - must be after ProtoMessage is defined -inline void ProtoLengthDelimited::decode_to_message(ProtoMessage &msg) const { +// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined +inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) const { msg.decode(this->value_, this->length_); } diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 93cea8133f..1420a15ff9 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -16,6 +16,8 @@ class UserServiceDescriptor { virtual ListEntitiesServicesResponse encode_list_service_response() = 0; virtual bool execute_service(const ExecuteServiceRequest &req) = 0; + + bool is_internal() { return false; } }; template T get_execute_arg_value(const ExecuteServiceArgument &arg); diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index 5c144cadcc..a1e9d464df 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -85,13 +85,13 @@ async def to_code(config): await cg.register_component(var, config) cg.add(var.set_active(config[CONF_ACTIVE])) - await esp32_ble_tracker.register_ble_device(var, config) + await esp32_ble_tracker.register_raw_ble_device(var, config) 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) cg.add(var.register_connection(connection_var)) - await esp32_ble_tracker.register_client(connection_var, connection_conf) + await esp32_ble_tracker.register_raw_client(connection_var, connection_conf) if config.get(CONF_CACHE_SERVICES): add_idf_sdkconfig_option("CONFIG_BT_GATTC_CACHE_NVS_FLASH", True) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 44d434802c..2bfccdb438 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -75,7 +75,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga resp.data.reserve(param->read.value_len); // Use bulk insert instead of individual push_backs resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len); - this->proxy_->get_api_connection()->send_message(resp); + this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); break; } case ESP_GATTC_WRITE_CHAR_EVT: @@ -89,7 +89,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTWriteResponse resp; resp.address = this->address_; resp.handle = param->write.handle; - this->proxy_->get_api_connection()->send_message(resp); + this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { @@ -103,7 +103,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->unreg_for_notify.handle; - this->proxy_->get_api_connection()->send_message(resp); + this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { @@ -116,7 +116,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->reg_for_notify.handle; - this->proxy_->get_api_connection()->send_message(resp); + this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); break; } case ESP_GATTC_NOTIFY_EVT: { @@ -128,7 +128,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga resp.data.reserve(param->notify.value_len); // Use bulk insert instead of individual push_backs resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len); - this->proxy_->get_api_connection()->send_message(resp); + this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); break; } default: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index a5e8ec0860..7d12842a24 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -3,6 +3,7 @@ #include "esphome/core/log.h" #include "esphome/core/macros.h" #include "esphome/core/application.h" +#include #ifdef USE_ESP32 @@ -24,9 +25,30 @@ std::vector get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])}; } +// 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; + +// 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, + "BLE advertisement data array size mismatch"); + BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } void BluetoothProxy::setup() { + // Pre-allocate response object + this->response_ = std::make_unique(); + + // 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->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { if (this->api_connection_ != nullptr) { this->send_bluetooth_scanner_state_(state); @@ -39,83 +61,86 @@ 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; - this->api_connection_->send_message(resp); + this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); } +#ifdef USE_ESP32_BLE_DEVICE bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_) - return false; - - ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), - device.get_rssi()); - this->send_api_packet_(device); - return true; + // This method should never be called since bluetooth_proxy always uses raw advertisements + // but we need to provide an implementation to satisfy the virtual method requirement + return false; } - -// 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; - -namespace { -// Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes) -// This is initialized at program startup before any threads -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -std::vector batch_buffer; -} // namespace - -static std::vector &get_batch_buffer() { return batch_buffer; } +#endif bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) { - if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) + if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) return false; - // Get the batch buffer reference - auto &batch_buffer = get_batch_buffer(); + auto &advertisements = this->response_->advertisements; - // Reserve additional capacity if needed - size_t new_size = batch_buffer.size() + count; - if (batch_buffer.capacity() < new_size) { - batch_buffer.reserve(new_size); - } - - // Add new advertisements to the batch buffer for (size_t i = 0; i < count; i++) { auto &result = scan_results[i]; uint8_t length = result.adv_data_len + result.scan_rsp_len; - batch_buffer.emplace_back(); - auto &adv = batch_buffer.back(); + // 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_]; adv.address = esp32_ble::ble_addr_to_uint64(result.bda); adv.rssi = result.rssi; adv.address_type = result.ble_addr_type; - adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]); + adv.data_len = length; + std::memcpy(adv.data, result.ble_adv, length); + + this->advertisement_count_++; 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); - } - // Only send if we've accumulated a good batch size to maximize batching efficiency - // https://github.com/esphome/backlog/issues/21 - if (batch_buffer.size() >= FLUSH_BATCH_SIZE) { - this->flush_pending_advertisements(); + // Flush if we have reached FLUSH_BATCH_SIZE + if (this->advertisement_count_ >= FLUSH_BATCH_SIZE) { + this->flush_pending_advertisements(); + } } return true; } void BluetoothProxy::flush_pending_advertisements() { - auto &batch_buffer = get_batch_buffer(); - if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr) + if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr) return; - api::BluetoothLERawAdvertisementsResponse resp; - resp.advertisements.swap(batch_buffer); - this->api_connection_->send_message(resp); + 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); + + // Reset count - existing items will be overwritten in next batch + this->advertisement_count_ = 0; } +#ifdef USE_ESP32_BLE_DEVICE void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { api::BluetoothLEAdvertisementResponse resp; resp.address = device.address_uint64(); @@ -151,16 +176,16 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi manufacturer_data.data.assign(data.data.begin(), data.data.end()); } - this->api_connection_->send_message(resp); + this->api_connection_->send_message(resp, api::BluetoothLEAdvertisementResponse::MESSAGE_TYPE); } +#endif // USE_ESP32_BLE_DEVICE void BluetoothProxy::dump_config() { ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); ESP_LOGCONFIG(TAG, " Active: %s\n" - " Connections: %d\n" - " Raw advertisements: %s", - YESNO(this->active_), this->connections_.size(), YESNO(this->raw_advertisements_)); + " Connections: %d", + YESNO(this->active_), this->connections_.size()); } int BluetoothProxy::get_bluetooth_connections_free() { @@ -188,15 +213,13 @@ void BluetoothProxy::loop() { } // Flush any pending BLE advertisements that have been accumulated but not yet sent - if (this->raw_advertisements_) { - static uint32_t last_flush_time = 0; - uint32_t now = App.get_loop_component_start_time(); + static uint32_t last_flush_time = 0; + uint32_t now = App.get_loop_component_start_time(); - // Flush accumulated advertisements every 100ms - if (now - last_flush_time >= 100) { - this->flush_pending_advertisements(); - last_flush_time = now; - } + // Flush accumulated advertisements every 100ms + if (now - last_flush_time >= 100) { + this->flush_pending_advertisements(); + last_flush_time = now; } for (auto *connection : this->connections_) { if (connection->send_service_ == connection->service_count_) { @@ -312,15 +335,13 @@ void BluetoothProxy::loop() { service_resp.characteristics.push_back(std::move(characteristic_resp)); } resp.services.push_back(std::move(service_resp)); - this->api_connection_->send_message(resp); + this->api_connection_->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); } } } esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() { - if (this->raw_advertisements_) - return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS; - return esp32_ble_tracker::AdvertisementParserType::PARSED_ADVERTISEMENTS; + return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS; } BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) { @@ -465,7 +486,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest call.success = ret == ESP_OK; call.error = ret; - this->api_connection_->send_message(call); + this->api_connection_->send_message(call, api::BluetoothDeviceClearCacheResponse::MESSAGE_TYPE); break; } @@ -565,7 +586,6 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection return; } this->api_connection_ = api_connection; - this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS; this->parent_->recalculate_advertisement_parser_types(); this->send_bluetooth_scanner_state_(this->parent_->get_scanner_state()); @@ -577,7 +597,6 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti return; } this->api_connection_ = nullptr; - this->raw_advertisements_ = false; this->parent_->recalculate_advertisement_parser_types(); } @@ -589,7 +608,7 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui call.connected = connected; call.mtu = mtu; call.error = error; - this->api_connection_->send_message(call); + this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE); } void BluetoothProxy::send_connections_free() { if (this->api_connection_ == nullptr) @@ -602,7 +621,7 @@ void BluetoothProxy::send_connections_free() { call.allocated.push_back(connection->address_); } } - this->api_connection_->send_message(call); + this->api_connection_->send_message(call, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE); } void BluetoothProxy::send_gatt_services_done(uint64_t address) { @@ -610,7 +629,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) { return; api::BluetoothGATTGetServicesDoneResponse call; call.address = address; - this->api_connection_->send_message(call); + this->api_connection_->send_message(call, api::BluetoothGATTGetServicesDoneResponse::MESSAGE_TYPE); } void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { @@ -620,7 +639,7 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_ call.address = address; call.handle = handle; call.error = error; - this->api_connection_->send_message(call); + this->api_connection_->send_message(call, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); } void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { @@ -629,7 +648,7 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_ call.paired = paired; call.error = error; - this->api_connection_->send_message(call); + this->api_connection_->send_message(call, api::BluetoothDevicePairingResponse::MESSAGE_TYPE); } void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { @@ -638,7 +657,7 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_e call.success = success; call.error = error; - this->api_connection_->send_message(call); + this->api_connection_->send_message(call, api::BluetoothDeviceUnpairingResponse::MESSAGE_TYPE); } void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index f0632350e0..52f1d0f88a 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -51,7 +51,9 @@ enum BluetoothProxySubscriptionFlag : uint32_t { class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: BluetoothProxy(); +#ifdef USE_ESP32_BLE_DEVICE bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +#endif bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override; void dump_config() override; void setup() override; @@ -129,7 +131,9 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com } protected: +#ifdef USE_ESP32_BLE_DEVICE void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); +#endif void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); BluetoothConnection *get_connection_(uint64_t address, bool reserve); @@ -141,9 +145,13 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com // Group 2: Container types (typically 12 bytes on 32-bit) std::vector connections_{}; + // BLE advertisement batching + std::vector advertisement_pool_; + std::unique_ptr response_; + // Group 3: 1-byte types grouped together bool active_; - bool raw_advertisements_{false}; + uint8_t advertisement_count_{0}; // 2 bytes used, 2 bytes padding }; diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index b084622f4c..5b40545d89 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -3,6 +3,7 @@ CODEOWNERS = ["@esphome/core"] CONF_BYTE_ORDER = "byte_order" +CONF_COLOR_DEPTH = "color_depth" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py index f9c0424104..658292ec7a 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_MODE, CONF_NUMBER, CONF_ON_VALUE, + CONF_SWITCH, CONF_TEXT, CONF_TRIGGER_ID, CONF_TYPE, @@ -33,7 +34,6 @@ CONF_LABEL = "label" CONF_MENU = "menu" CONF_BACK = "back" CONF_SELECT = "select" -CONF_SWITCH = "switch" CONF_ON_TEXT = "on_text" CONF_OFF_TEXT = "off_text" CONF_VALUE_LAMBDA = "value_lambda" diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index b8fa73b707..e663a3d0fc 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -4,6 +4,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/core/log.h" #include "esphome/core/util.h" +#include "esphome/core/helpers.h" #include #include @@ -71,7 +72,11 @@ bool E131Component::join_igmp_groups_() { ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)); - auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr); + err_t err; + { + LwIPLock lock; + err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr); + } if (err) { ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first); @@ -104,6 +109,7 @@ void E131Component::leave_(int universe) { if (listen_method_ == E131_MULTICAST) { ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)); + LwIPLock lock; igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr); } diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index fdc469e419..c772a3438c 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -39,7 +39,7 @@ import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed from esphome.types import ConfigType -from .boards import BOARDS +from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa KEY_BOARD, KEY_COMPONENTS, @@ -487,25 +487,32 @@ def _platform_is_platformio(value): def _detect_variant(value): - board = value[CONF_BOARD] - if board in BOARDS: - variant = BOARDS[board][KEY_VARIANT] - if CONF_VARIANT in value and variant != value[CONF_VARIANT]: + board = value.get(CONF_BOARD) + variant = value.get(CONF_VARIANT) + if variant and board is None: + # If variant is set, we can derive the board from it + # variant has already been validated against the known set + value = value.copy() + value[CONF_BOARD] = STANDARD_BOARDS[variant] + elif board in BOARDS: + variant = variant or BOARDS[board][KEY_VARIANT] + if variant != BOARDS[board][KEY_VARIANT]: raise cv.Invalid( f"Option '{CONF_VARIANT}' does not match selected board.", path=[CONF_VARIANT], ) value = value.copy() value[CONF_VARIANT] = variant + elif not variant: + raise cv.Invalid( + "This board is unknown, if you are sure you want to compile with this board selection, " + f"override with option '{CONF_VARIANT}'", + path=[CONF_BOARD], + ) else: - if CONF_VARIANT not in value: - raise cv.Invalid( - "This board is unknown, if you are sure you want to compile with this board selection, " - f"override with option '{CONF_VARIANT}'", - path=[CONF_BOARD], - ) _LOGGER.warning( - "This board is unknown. Make sure the chosen chip component is correct.", + "This board is unknown; the specified variant '%s' will be used but this may not work as expected.", + variant, ) return value @@ -676,7 +683,7 @@ CONF_PARTITIONS = "partitions" CONFIG_SCHEMA = cv.All( cv.Schema( { - cv.Required(CONF_BOARD): cv.string_strict, + cv.Optional(CONF_BOARD): cv.string_strict, cv.Optional(CONF_CPU_FREQUENCY): cv.one_of( *FULL_CPU_FREQUENCIES, upper=True ), @@ -691,6 +698,7 @@ CONFIG_SCHEMA = cv.All( _detect_variant, _set_default_framework, set_core_data, + cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT), ) diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 68fee48830..cf6cf8cbe5 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -2,13 +2,30 @@ from .const import ( VARIANT_ESP32, VARIANT_ESP32C2, VARIANT_ESP32C3, + VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + VARIANTS, ) +STANDARD_BOARDS = { + VARIANT_ESP32: "esp32dev", + VARIANT_ESP32C2: "esp32-c2-devkitm-1", + VARIANT_ESP32C3: "esp32-c3-devkitm-1", + VARIANT_ESP32C5: "esp32-c5-devkitc-1", + VARIANT_ESP32C6: "esp32-c6-devkitm-1", + VARIANT_ESP32H2: "esp32-h2-devkitm-1", + VARIANT_ESP32P4: "esp32-p4-evboard", + VARIANT_ESP32S2: "esp32-s2-kaluga-1", + VARIANT_ESP32S3: "esp32-s3-devkitc-1", +} + +# Make sure not missed here if a new variant added. +assert all(v in STANDARD_BOARDS for v in VARIANTS) + ESP32_BASE_PINS = { "TX": 1, "RX": 3, diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp index 310e7bd94a..051b7ce162 100644 --- a/esphome/components/esp32/helpers.cpp +++ b/esphome/components/esp32/helpers.cpp @@ -1,4 +1,5 @@ #include "esphome/core/helpers.h" +#include "esphome/core/defines.h" #ifdef USE_ESP32 @@ -30,6 +31,45 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); } IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING +#include "lwip/priv/tcpip_priv.h" +#endif + +LwIPLock::LwIPLock() { +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING + // When CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled, lwIP uses a global mutex to protect + // its internal state. Any thread can take this lock to safely access lwIP APIs. + // + // sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) returns true if the current thread + // already holds the lwIP core lock. This prevents recursive locking attempts and + // allows nested LwIPLock instances to work correctly. + // + // If we don't already hold the lock, acquire it. This will block until the lock + // is available if another thread currently holds it. + if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { + LOCK_TCPIP_CORE(); + } +#endif +} + +LwIPLock::~LwIPLock() { +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING + // Only release the lwIP core lock if this thread currently holds it. + // + // sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) queries lwIP's internal lock + // ownership tracking. It returns true only if the current thread is registered + // as the lock holder. + // + // This check is essential because: + // 1. We may not have acquired the lock in the constructor (if we already held it) + // 2. The lock might have been released by other means between constructor and destructor + // 3. Calling UNLOCK_TCPIP_CORE() without holding the lock causes undefined behavior + if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { + UNLOCK_TCPIP_CORE(); + } +#endif +} + void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #if defined(CONFIG_SOC_IEEE802154_SUPPORTED) // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 7d0a3bbfd5..bf425b3730 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -105,6 +105,7 @@ void BLEClientBase::dump_config() { } } +#ifdef USE_ESP32_BLE_DEVICE bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { if (!this->auto_connect_) return false; @@ -122,6 +123,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { this->remote_addr_type_ = device.get_address_type(); return true; } +#endif void BLEClientBase::connect() { ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(), diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index bf3b589b1b..457a88ec1d 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -31,7 +31,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void dump_config() override; void run_later(std::function &&f); // NOLINT +#ifdef USE_ESP32_BLE_DEVICE bool parse_device(const espbt::ESPBTDevice &device) override; +#endif void on_scan_end() override {} bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 547cf84ed1..68f4657515 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -31,6 +31,8 @@ from esphome.const import ( CONF_TRIGGER_ID, ) from esphome.core import CORE +from esphome.enum import StrEnum +from esphome.types import ConfigType AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] @@ -50,6 +52,25 @@ IDF_MAX_CONNECTIONS = 9 _LOGGER = logging.getLogger(__name__) + +# Enum for BLE features +class BLEFeatures(StrEnum): + ESP_BT_DEVICE = "ESP_BT_DEVICE" + + +# Set to track which features are needed by components +_required_features: set[BLEFeatures] = set() + + +def register_ble_features(features: set[BLEFeatures]) -> None: + """Register BLE features that a component needs. + + Args: + features: Set of BLEFeatures enum members + """ + _required_features.update(features) + + esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") ESP32BLETracker = esp32_ble_tracker_ns.class_( "ESP32BLETracker", @@ -277,6 +298,15 @@ async def to_code(config): cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625))) cg.add(var.set_scan_active(params[CONF_ACTIVE])) cg.add(var.set_scan_continuous(params[CONF_CONTINUOUS])) + + # Register ESP_BT_DEVICE feature if any of the automation triggers are used + if ( + config.get(CONF_ON_BLE_ADVERTISE) + or config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE) + or config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE) + ): + register_ble_features({BLEFeatures.ESP_BT_DEVICE}) + for conf in config.get(CONF_ON_BLE_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if CONF_MAC_ADDRESS in conf: @@ -334,6 +364,11 @@ async def to_code(config): cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") + + # Add feature-specific defines based on what's needed + if BLEFeatures.ESP_BT_DEVICE in _required_features: + cg.add_define("USE_ESP32_BLE_DEVICE") + if config.get(CONF_SOFTWARE_COEXISTENCE): cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE") @@ -382,13 +417,43 @@ async def esp32_ble_tracker_stop_scan_action_to_code( return var -async def register_ble_device(var, config): +async def register_ble_device( + var: cg.SafeExpType, config: ConfigType +) -> cg.SafeExpType: + register_ble_features({BLEFeatures.ESP_BT_DEVICE}) paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) cg.add(paren.register_listener(var)) return var -async def register_client(var, config): +async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType: + register_ble_features({BLEFeatures.ESP_BT_DEVICE}) + paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) + cg.add(paren.register_client(var)) + return var + + +async def register_raw_ble_device( + var: cg.SafeExpType, config: ConfigType +) -> cg.SafeExpType: + """Register a BLE device listener that only needs raw advertisement data. + + This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice + will not be compiled in if this is the only registration method used. + """ + paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) + cg.add(paren.register_listener(var)) + return var + + +async def register_raw_client( + var: cg.SafeExpType, config: ConfigType +) -> cg.SafeExpType: + """Register a BLE client that only needs raw advertisement data. + + This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice + will not be compiled in if this is the only registration method used. + """ 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 6bef9edcb3..ef677922e3 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -7,6 +7,7 @@ namespace esphome { namespace esp32_ble_tracker { +#ifdef USE_ESP32_BLE_DEVICE class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { public: explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } @@ -87,6 +88,7 @@ class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener { bool parse_device(const ESPBTDevice &device) override { return false; } void on_scan_end() override { this->trigger(); } }; +#endif // USE_ESP32_BLE_DEVICE template class ESP32BLEStartScanAction : public Action { public: diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index d950ccb5f1..44577afbbd 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -141,6 +141,7 @@ void ESP32BLETracker::loop() { } if (this->parse_advertisements_) { +#ifdef USE_ESP32_BLE_DEVICE ESPBTDevice device; device.parse_scan_rst(scan_result); @@ -162,6 +163,7 @@ void ESP32BLETracker::loop() { if (!found && !this->scan_continuous_) { this->print_bt_device_info(device); } +#endif // USE_ESP32_BLE_DEVICE } // Move to next entry in ring buffer @@ -511,6 +513,7 @@ void ESP32BLETracker::set_scanner_state_(ScannerState state) { this->scanner_state_callbacks_.call(state); } +#ifdef USE_ESP32_BLE_DEVICE ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } optional ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { if (!data.uuid.contains(0x4C, 0x00)) @@ -751,13 +754,16 @@ 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]); return mac; } + uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); } +#endif // USE_ESP32_BLE_DEVICE void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, "BLE Tracker:"); @@ -796,6 +802,7 @@ void ESP32BLETracker::dump_config() { } } +#ifdef USE_ESP32_BLE_DEVICE void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { const uint64_t address = device.address_uint64(); for (auto &disc : this->already_discovered_) { @@ -866,8 +873,9 @@ 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 } // namespace esp32_ble_tracker } // namespace esphome -#endif +#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 f5ed75a93e..e10f4551e8 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -39,6 +39,7 @@ struct ServiceData { adv_data_t data; }; +#ifdef USE_ESP32_BLE_DEVICE class ESPBLEiBeacon { public: ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); } @@ -116,13 +117,16 @@ class ESPBTDevice { std::vector service_datas_{}; const BLEScanResult *scan_result_{nullptr}; }; +#endif // USE_ESP32_BLE_DEVICE class ESP32BLETracker; class ESPBTDeviceListener { public: virtual void on_scan_end() {} +#ifdef USE_ESP32_BLE_DEVICE virtual bool parse_device(const ESPBTDevice &device) = 0; +#endif virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; }; virtual AdvertisementParserType get_advertisement_parser_type() { return AdvertisementParserType::PARSED_ADVERTISEMENTS; @@ -237,7 +241,9 @@ class ESP32BLETracker : public Component, void register_client(ESPBTClient *client); void recalculate_advertisement_parser_types(); +#ifdef USE_ESP32_BLE_DEVICE void print_bt_device_info(const ESPBTDevice &device); +#endif void start_scan(); void stop_scan(); diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 6e36f7d5a7..43e71df432 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -1,3 +1,5 @@ +import logging + from esphome import automation, pins import esphome.codegen as cg from esphome.components import i2c @@ -8,6 +10,7 @@ from esphome.const import ( CONF_CONTRAST, CONF_DATA_PINS, CONF_FREQUENCY, + CONF_I2C, CONF_I2C_ID, CONF_ID, CONF_PIN, @@ -20,6 +23,9 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.core.entity_helpers import setup_entity +import esphome.final_validate as fv + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["esp32"] @@ -113,6 +119,12 @@ ENUM_SPECIAL_EFFECT = { "SEPIA": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_SEPIA, } +camera_fb_location_t = cg.global_ns.enum("camera_fb_location_t") +ENUM_FB_LOCATION = { + "PSRAM": cg.global_ns.CAMERA_FB_IN_PSRAM, + "DRAM": cg.global_ns.CAMERA_FB_IN_DRAM, +} + # pin assignment CONF_HREF_PIN = "href_pin" CONF_PIXEL_CLOCK_PIN = "pixel_clock_pin" @@ -143,6 +155,7 @@ CONF_MAX_FRAMERATE = "max_framerate" CONF_IDLE_FRAMERATE = "idle_framerate" # frame buffer CONF_FRAME_BUFFER_COUNT = "frame_buffer_count" +CONF_FRAME_BUFFER_LOCATION = "frame_buffer_location" # stream trigger CONF_ON_STREAM_START = "on_stream_start" @@ -224,6 +237,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_ON_STREAM_START): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -250,6 +266,22 @@ CONFIG_SCHEMA = cv.All( cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), ) + +def _final_validate(config): + if CONF_I2C_PINS not in config: + return + fconf = fv.full_config.get() + if fconf.get(CONF_I2C): + raise cv.Invalid( + "The `i2c_pins:` config option is incompatible with an dedicated `i2c:` block, use `i2c_id` instead" + ) + _LOGGER.warning( + "The `i2c_pins:` config option is deprecated. Use `i2c_id:` with a dedicated `i2c:` definition instead." + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + SETTERS = { # pin assignment CONF_DATA_PINS: "set_data_pins", @@ -279,6 +311,7 @@ SETTERS = { CONF_WB_MODE: "set_wb_mode", # test pattern CONF_TEST_PATTERN: "set_test_pattern", + CONF_FRAME_BUFFER_LOCATION: "set_frame_buffer_location", } @@ -306,6 +339,7 @@ async def to_code(config): else: cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE])) cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT])) + cg.add(var.set_frame_buffer_location(config[CONF_FRAME_BUFFER_LOCATION])) cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add_define("USE_CAMERA") diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index eadb8a4408..38bd8d5822 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -133,6 +133,7 @@ void ESP32Camera::dump_config() { ESP_LOGCONFIG(TAG, " JPEG Quality: %u\n" " Framebuffer Count: %u\n" + " Framebuffer Location: %s\n" " Contrast: %d\n" " Brightness: %d\n" " Saturation: %d\n" @@ -140,8 +141,9 @@ void ESP32Camera::dump_config() { " Horizontal Mirror: %s\n" " Special Effect: %u\n" " White Balance Mode: %u", - st.quality, conf.fb_count, st.contrast, st.brightness, st.saturation, ONOFF(st.vflip), - ONOFF(st.hmirror), st.special_effect, st.wb_mode); + st.quality, conf.fb_count, this->config_.fb_location == CAMERA_FB_IN_PSRAM ? "PSRAM" : "DRAM", + st.contrast, st.brightness, st.saturation, ONOFF(st.vflip), ONOFF(st.hmirror), st.special_effect, + st.wb_mode); // ESP_LOGCONFIG(TAG, " Auto White Balance: %u", st.awb); // ESP_LOGCONFIG(TAG, " Auto White Balance Gain: %u", st.awb_gain); ESP_LOGCONFIG(TAG, @@ -350,6 +352,9 @@ void ESP32Camera::set_frame_buffer_count(uint8_t fb_count) { this->config_.fb_count = fb_count; this->set_frame_buffer_mode(fb_count > 1 ? CAMERA_GRAB_LATEST : CAMERA_GRAB_WHEN_EMPTY); } +void ESP32Camera::set_frame_buffer_location(camera_fb_location_t fb_location) { + this->config_.fb_location = fb_location; +} /* ---------------- public API (specific) ---------------- */ void ESP32Camera::add_image_callback(std::function)> &&callback) { diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 8ce3faf039..0e7f7c0ea6 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -152,6 +152,7 @@ class ESP32Camera : public camera::Camera { /* -- frame buffer */ void set_frame_buffer_mode(camera_grab_mode_t mode); void set_frame_buffer_count(uint8_t fb_count); + void set_frame_buffer_location(camera_fb_location_t fb_location); /* public API (derivated) */ void setup() override; diff --git a/esphome/components/esp8266/helpers.cpp b/esphome/components/esp8266/helpers.cpp index 993de710c6..036594fa17 100644 --- a/esphome/components/esp8266/helpers.cpp +++ b/esphome/components/esp8266/helpers.cpp @@ -22,6 +22,10 @@ void Mutex::unlock() {} IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } +// ESP8266 doesn't support lwIP core locking, so this is a no-op +LwIPLock::LwIPLock() {} +LwIPLock::~LwIPLock() {} + void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) wifi_get_macaddr(STATION_IF, mac); } diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index f8c2f3a72e..ff37dcfdd1 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -420,6 +420,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() { } network::IPAddress EthernetComponent::get_dns_address(uint8_t num) { + LwIPLock lock; const ip_addr_t *dns_ip = dns_getserver(num); return dns_ip; } @@ -527,6 +528,7 @@ void EthernetComponent::start_connect_() { ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); if (this->manual_ip_.has_value()) { + LwIPLock lock; if (this->manual_ip_->dns1.is_set()) { ip_addr_t d; d = this->manual_ip_->dns1; @@ -559,8 +561,13 @@ bool EthernetComponent::is_connected() { return this->state_ == EthernetComponen void EthernetComponent::dump_connect_params_() { esp_netif_ip_info_t ip; esp_netif_get_ip_info(this->eth_netif_, &ip); - const ip_addr_t *dns_ip1 = dns_getserver(0); - const ip_addr_t *dns_ip2 = dns_getserver(1); + const ip_addr_t *dns_ip1; + const ip_addr_t *dns_ip2; + { + LwIPLock lock; + dns_ip1 = dns_getserver(0); + dns_ip2 = dns_getserver(1); + } ESP_LOGCONFIG(TAG, " IP Address: %s\n" diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h index 2e67694bbd..2adc08e31e 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -29,7 +29,6 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS } float get_setup_priority() const override { return setup_priority::ETHERNET; } - std::string unique_id() override { return get_mac_address() + "-ethernetinfo"; } void dump_config() override; void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } @@ -52,7 +51,6 @@ class DNSAddressEthernetInfo : public PollingComponent, public text_sensor::Text } } float get_setup_priority() const override { return setup_priority::ETHERNET; } - std::string unique_id() override { return get_mac_address() + "-ethernetinfo-dns"; } void dump_config() override; protected: @@ -63,7 +61,6 @@ class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor public: void setup() override { this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty()); } float get_setup_priority() const override { return setup_priority::ETHERNET; } - std::string unique_id() override { return get_mac_address() + "-ethernetinfo-mac"; } void dump_config() override; }; diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 867a8efe49..59f54520fa 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -29,7 +29,21 @@ CONFIG_SCHEMA = ( .extend( { cv.Required(CONF_PIN): pins.gpio_input_pin_schema, - cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean, + # Interrupts are disabled by default for bk72xx, ln882x, and rtl87xx platforms + # 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. + cv.SplitDefault( + CONF_USE_INTERRUPT, + bk72xx=False, + esp32=True, + esp8266=True, + host=True, + ln882x=False, + nrf52=True, + rp2040=True, + rtl87xx=False, + ): cv.boolean, cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum( INTERRUPT_TYPES, upper=True ), diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index e4643405ce..c57d537bdb 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -7,6 +7,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) #define SOC_HP_I2C_NUM SOC_I2C_NUM @@ -20,21 +21,72 @@ static const char *const TAG = "i2c.idf"; void IDFI2CBus::setup() { ESP_LOGCONFIG(TAG, "Running setup"); static i2c_port_t next_port = I2C_NUM_0; - port_ = next_port; + this->port_ = next_port; + if (this->port_ == I2C_NUM_MAX) { + ESP_LOGE(TAG, "No more than %u buses supported", I2C_NUM_MAX); + this->mark_failed(); + return; + } + + if (this->timeout_ > 13000) { + ESP_LOGW(TAG, "Using max allowed timeout: 13 ms"); + this->timeout_ = 13000; + } + + 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{}; + memset(&bus_conf, 0, sizeof(bus_conf)); + bus_conf.sda_io_num = gpio_num_t(sda_pin_); + bus_conf.scl_io_num = gpio_num_t(scl_pin_); + bus_conf.i2c_port = this->port_; + bus_conf.glitch_ignore_cnt = 7; +#if SOC_LP_I2C_SUPPORTED + if (this->port_ < SOC_HP_I2C_NUM) { + bus_conf.clk_source = I2C_CLK_SRC_DEFAULT; + } else { + bus_conf.lp_source_clk = LP_I2C_SCLK_DEFAULT; + } +#else + bus_conf.clk_source = I2C_CLK_SRC_DEFAULT; +#endif + bus_conf.flags.enable_internal_pullup = sda_pullup_enabled_ || scl_pullup_enabled_; + esp_err_t err = i2c_new_master_bus(&bus_conf, &this->bus_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + + i2c_device_config_t dev_conf{}; + memset(&dev_conf, 0, sizeof(dev_conf)); + dev_conf.dev_addr_length = I2C_ADDR_BIT_LEN_7; + dev_conf.device_address = I2C_DEVICE_ADDRESS_NOT_USED; + dev_conf.scl_speed_hz = this->frequency_; + dev_conf.scl_wait_us = this->timeout_; + err = i2c_master_bus_add_device(this->bus_, &dev_conf, &this->dev_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + + this->initialized_ = true; + + if (this->scan_) { + ESP_LOGV(TAG, "Scanning for devices"); + 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 - if (port_ == I2C_NUM_MAX) { - ESP_LOGE(TAG, "No more than %u buses supported", SOC_HP_I2C_NUM); - this->mark_failed(); - return; - } - - recover_(); - i2c_config_t conf{}; memset(&conf, 0, sizeof(conf)); conf.mode = I2C_MODE_MASTER; @@ -53,11 +105,7 @@ void IDFI2CBus::setup() { this->mark_failed(); return; } - if (timeout_ > 0) { // if timeout specified in yaml: - if (timeout_ > 13000) { - ESP_LOGW(TAG, "i2c timeout of %" PRIu32 "us greater than max of 13ms on esp-idf, setting to max", timeout_); - timeout_ = 13000; - } + 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)); @@ -73,12 +121,15 @@ void IDFI2CBus::setup() { 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() { ESP_LOGCONFIG(TAG, "I2C Bus:"); ESP_LOGCONFIG(TAG, @@ -123,6 +174,74 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { ESP_LOGVV(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; + } + 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++; + } + 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].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, "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; + } +#else i2c_cmd_handle_t cmd = i2c_cmd_link_create(); esp_err_t err = i2c_master_start(cmd); if (err != ESP_OK) { @@ -168,6 +287,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { 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]; @@ -185,6 +305,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { 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 @@ -207,6 +328,49 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b 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) { @@ -252,6 +416,7 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); return ERROR_UNKNOWN; } +#endif return ERROR_OK; } diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index ee29578944..8d325de6bc 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -2,9 +2,14 @@ #ifdef USE_ESP_IDF -#include #include "esphome/core/component.h" #include "i2c_bus.h" +#include "esp_idf_version.h" +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) +#include +#else +#include +#endif namespace esphome { namespace i2c { @@ -38,6 +43,10 @@ class IDFI2CBus : public InternalI2CBus, public Component { 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_; +#endif i2c_port_t port_; uint8_t sda_pin_; bool sda_pullup_enabled_; diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index 5f66f2e962..633bd0e7dd 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -36,8 +36,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub #ifdef USE_I2S_LEGACY #if SOC_I2S_SUPPORTS_ADC - void set_adc_channel(adc1_channel_t channel) { - this->adc_channel_ = channel; + void set_adc_channel(adc_channel_t channel) { + this->adc_channel_ = (adc1_channel_t) channel; this->adc_ = true; } #endif diff --git a/esphome/components/libretiny/helpers.cpp b/esphome/components/libretiny/helpers.cpp index b6451860d5..37ae0fb455 100644 --- a/esphome/components/libretiny/helpers.cpp +++ b/esphome/components/libretiny/helpers.cpp @@ -26,6 +26,10 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); } IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } +// LibreTiny doesn't support lwIP core locking, so this is a no-op +LwIPLock::LwIPLock() {} +LwIPLock::~LwIPLock() {} + void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) WiFi.macAddress(mac); } diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 9ac2999696..e79396da04 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -21,6 +21,11 @@ from esphome.components.libretiny.const import ( COMPONENT_LN882X, COMPONENT_RTL87XX, ) +from esphome.components.zephyr import ( + zephyr_add_cdc_acm, + zephyr_add_overlay, + zephyr_add_prj_conf, +) from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -41,6 +46,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_LN882X, + PLATFORM_NRF52, PLATFORM_RP2040, PLATFORM_RTL87XX, PlatformFramework, @@ -115,6 +121,8 @@ ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG] UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] +UART_SELECTION_NRF52 = [USB_CDC, UART0] + HARDWARE_UART_TO_UART_SELECTION = { UART0: logger_ns.UART_SELECTION_UART0, UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, @@ -167,6 +175,8 @@ def uart_selection(value): return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) if CORE.is_host: raise cv.Invalid("Uart selection not valid for host platform") + if CORE.is_nrf52: + return cv.one_of(*UART_SELECTION_NRF52, upper=True)(value) raise NotImplementedError @@ -183,9 +193,10 @@ def validate_local_no_higher_than_global(value): Logger = logger_ns.class_("Logger", cg.Component) LoggerMessageTrigger = logger_ns.class_( "LoggerMessageTrigger", - automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr), + automation.Trigger.template(cg.uint8, cg.const_char_ptr, cg.const_char_ptr), ) + CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash" CONFIG_SCHEMA = cv.All( cv.Schema( @@ -227,6 +238,7 @@ CONFIG_SCHEMA = cv.All( bk72xx=DEFAULT, ln882x=DEFAULT, rtl87xx=DEFAULT, + nrf52=USB_CDC, ): cv.All( cv.only_on( [ @@ -236,6 +248,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX, + PLATFORM_NRF52, ] ), uart_selection, @@ -358,6 +371,15 @@ async def to_code(config): except cv.Invalid: pass + if CORE.using_zephyr: + if config[CONF_HARDWARE_UART] == UART0: + zephyr_add_overlay("""&uart0 { status = "okay";};""") + if config[CONF_HARDWARE_UART] == UART1: + zephyr_add_overlay("""&uart1 { status = "okay";};""") + if config[CONF_HARDWARE_UART] == USB_CDC: + zephyr_add_prj_conf("UART_LINE_CTRL", True) + zephyr_add_cdc_acm(config, 0) + # Register at end for safe mode await cg.register_component(log, config) @@ -368,7 +390,7 @@ async def to_code(config): await automation.build_automation( trigger, [ - (cg.int_, "level"), + (cg.uint8, "level"), (cg.const_char_ptr, "tag"), (cg.const_char_ptr, "message"), ], @@ -462,6 +484,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, + "logger_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, "task_log_buffer.cpp": { PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index db807f7e53..01a7565699 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -4,9 +4,9 @@ #include // For unique_ptr #endif +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" namespace esphome { namespace logger { @@ -160,6 +160,8 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT #if defined(USE_ESP32) || defined(USE_LIBRETINY) this->main_task_ = xTaskGetCurrentTaskHandle(); +#elif defined(USE_ZEPHYR) + this->main_task_ = k_current_get(); #endif } #ifdef USE_ESPHOME_TASK_LOG_BUFFER @@ -172,6 +174,7 @@ 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) @@ -185,8 +188,13 @@ void Logger::loop() { } opened = !opened; } +#endif + this->process_messages_(); +} +#endif #endif +void Logger::process_messages_() { #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Process any buffered messages when available if (this->log_buffer_->has_messages()) { @@ -227,12 +235,11 @@ void Logger::loop() { } #endif } -#endif 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; } -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) UARTSelection Logger::get_uart() const { return this->uart_; } #endif diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index fb68e75a51..6bd5bb66ed 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -29,6 +29,11 @@ #include #endif // USE_ESP_IDF +#ifdef USE_ZEPHYR +#include +struct device; +#endif + namespace esphome { namespace logger { @@ -56,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = { "VV", // VERY_VERBOSE }; -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * * Advanced configuration (pin selection, etc) is not supported. @@ -82,7 +87,7 @@ enum UARTSelection : uint8_t { UART_SELECTION_UART0_SWAP, #endif // USE_ESP8266 }; -#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY +#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY || USE_ZEPHYR /** * @brief Logger component for all ESPHome logging. @@ -107,7 +112,7 @@ 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) +#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) || defined(USE_ZEPHYR) void loop() override; #endif /// Manually set the baud rate for serial, set to 0 to disable. @@ -122,7 +127,7 @@ class Logger : public Component { #ifdef USE_ESP32 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) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. UARTSelection get_uart() const; @@ -157,6 +162,7 @@ class Logger : public Component { #endif protected: + void process_messages_(); void write_msg_(const char *msg); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator @@ -164,7 +170,7 @@ class Logger : public Component { 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) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); #else this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); @@ -231,7 +237,10 @@ class Logger : public Component { #ifdef USE_ARDUINO Stream *hw_serial_{nullptr}; #endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ZEPHYR) + const device *uart_dev_{nullptr}; +#endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) void *main_task_ = nullptr; // Only used for thread name identification #endif #ifdef USE_ESP32 @@ -256,7 +265,7 @@ class Logger : public Component { uint16_t tx_buffer_at_{0}; uint16_t tx_buffer_size_{0}; uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; -#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) UARTSelection uart_{UART_SELECTION_UART0}; #endif #ifdef USE_LIBRETINY @@ -268,9 +277,13 @@ class Logger : public Component { bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) const char *HOT get_thread_name_() { +#ifdef USE_ZEPHYR + k_tid_t current_task = k_current_get(); +#else TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); +#endif if (current_task == main_task_) { return nullptr; // Main task } else { @@ -278,6 +291,8 @@ class Logger : public Component { return pcTaskGetName(current_task); #elif defined(USE_LIBRETINY) return pcTaskGetTaskName(current_task); +#elif defined(USE_ZEPHYR) + return k_thread_name_get(current_task); #endif } } @@ -319,7 +334,7 @@ class Logger : public Component { const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level]; -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#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, diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp new file mode 100644 index 0000000000..35ef2e9561 --- /dev/null +++ b/esphome/components/logger/logger_zephyr.cpp @@ -0,0 +1,88 @@ +#ifdef USE_ZEPHYR + +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "logger.h" + +#include +#include +#include + +namespace esphome { +namespace logger { + +static const char *const TAG = "logger"; + +void Logger::loop() { +#ifdef USE_LOGGER_USB_CDC + if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) { + return; + } + static bool opened = false; + uint32_t dtr = 0; + uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr); + + /* Poll if the DTR flag was set, optional */ + if (opened == dtr) { + return; + } + + if (!opened) { + App.schedule_dump_config(); + } + opened = !opened; +#endif + this->process_messages_(); +} + +void Logger::pre_setup() { + if (this->baud_rate_ > 0) { + static const struct device *uart_dev = nullptr; + switch (this->uart_) { + case UART_SELECTION_UART0: + uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0)); + break; + case UART_SELECTION_UART1: + uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1)); + break; +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0)); + if (device_is_ready(uart_dev)) { + usb_enable(nullptr); + } + break; +#endif + } + if (!device_is_ready(uart_dev)) { + ESP_LOGE(TAG, "%s is not ready.", get_uart_selection_()); + } else { + this->uart_dev_ = uart_dev; + } + } + global_logger = this; + ESP_LOGI(TAG, "Log initialized"); +} + +void HOT Logger::write_msg_(const char *msg) { +#ifdef CONFIG_PRINTK + printk("%s\n", msg); +#endif + if (nullptr == this->uart_dev_) { + return; + } + while (*msg) { + uart_poll_out(this->uart_dev_, *msg); + ++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_]; } + +} // namespace logger +} // namespace esphome + +#endif diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 426dd3f229..11d7bca5fa 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -76,6 +76,7 @@ async def theme_to_code(config): for w_name, style in theme.items(): # Work around Python 3.10 bug with nested async comprehensions # With Python 3.11 this could be simplified + # TODO: Now that we require Python 3.11+, this can be updated to use nested comprehensions styles = {} for part, states in collect_parts(style).items(): styles[part] = { diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 40e69119f0..10b6f63528 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -192,7 +192,7 @@ class WidgetType: class NumberType(WidgetType): def get_max(self, config: dict): - return int(config[CONF_MAX_VALUE] or 100) + return int(config.get(CONF_MAX_VALUE, 100)) def get_min(self, config: dict): - return int(config[CONF_MIN_VALUE] or 0) + return int(config.get(CONF_MIN_VALUE, 0)) diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index f836a1eca5..acec986f99 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_VALUE, CONF_WIDTH, ) +from esphome.cpp_generator import IntLiteral from ..automation import action_to_code from ..defines import ( @@ -188,6 +189,8 @@ class MeterType(WidgetType): 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 with LocalVariable( "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) ) as meter_var: @@ -264,7 +267,7 @@ class MeterType(WidgetType): color_start, color_end, v[CONF_LOCAL], - size.process(v[CONF_WIDTH]), + await size.process(v[CONF_WIDTH]), ), ) if t == CONF_IMAGE: diff --git a/esphome/components/lvgl/widgets/switch.py b/esphome/components/lvgl/widgets/switch.py index a7c1356bf2..06738faae5 100644 --- a/esphome/components/lvgl/widgets/switch.py +++ b/esphome/components/lvgl/widgets/switch.py @@ -1,9 +1,9 @@ +from esphome.const import CONF_SWITCH + from ..defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN from ..types import LvBoolean from . import WidgetType -CONF_SWITCH = "switch" - class SwitchType(WidgetType): def __init__(self): diff --git a/esphome/components/mipi_spi/__init__.py b/esphome/components/mipi_spi/__init__.py index 46b0206a1f..879efda619 100644 --- a/esphome/components/mipi_spi/__init__.py +++ b/esphome/components/mipi_spi/__init__.py @@ -2,10 +2,8 @@ CODEOWNERS = ["@clydebarrow"] DOMAIN = "mipi_spi" -CONF_DRAW_FROM_ORIGIN = "draw_from_origin" CONF_SPI_16 = "spi_16" CONF_PIXEL_MODE = "pixel_mode" -CONF_COLOR_DEPTH = "color_depth" CONF_BUS_MODE = "bus_mode" CONF_USE_AXIS_FLIPS = "use_axis_flips" CONF_NATIVE_WIDTH = "native_width" diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 061257e859..d25dfd8539 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -3,11 +3,18 @@ import logging from esphome import pins import esphome.codegen as cg from esphome.components import display, spi +from esphome.components.const import ( + CONF_BYTE_ORDER, + CONF_COLOR_DEPTH, + CONF_DRAW_ROUNDING, +) +from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE import esphome.config_validation as cv from esphome.config_validation import ALLOW_EXTRA from esphome.const import ( CONF_BRIGHTNESS, + CONF_BUFFER_SIZE, CONF_COLOR_ORDER, CONF_CS_PIN, CONF_DATA_RATE, @@ -24,19 +31,19 @@ from esphome.const import ( CONF_MODEL, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, + CONF_PAGES, CONF_RESET_PIN, CONF_ROTATION, CONF_SWAP_XY, CONF_TRANSFORM, CONF_WIDTH, ) -from esphome.core import TimePeriod +from esphome.core import CORE, TimePeriod +from esphome.cpp_generator import TemplateArguments +from esphome.final_validate import full_config -from ..const import CONF_DRAW_ROUNDING -from ..lvgl.defines import CONF_COLOR_DEPTH from . import ( CONF_BUS_MODE, - CONF_DRAW_FROM_ORIGIN, CONF_NATIVE_HEIGHT, CONF_NATIVE_WIDTH, CONF_PIXEL_MODE, @@ -55,6 +62,7 @@ from .models import ( MADCTL_XFLIP, MADCTL_YFLIP, DriverChip, + adafruit, amoled, cyd, ili, @@ -69,43 +77,112 @@ DEPENDENCIES = ["spi"] LOGGER = logging.getLogger(DOMAIN) mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") -MipiSpi = mipi_spi_ns.class_( - "MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice +MipiSpi = mipi_spi_ns.class_("MipiSpi", display.Display, cg.Component, spi.SPIDevice) +MipiSpiBuffer = mipi_spi_ns.class_( + "MipiSpiBuffer", MipiSpi, display.Display, cg.Component, spi.SPIDevice ) ColorOrder = display.display_ns.enum("ColorMode") ColorBitness = display.display_ns.enum("ColorBitness") Model = mipi_spi_ns.enum("Model") +PixelMode = mipi_spi_ns.enum("PixelMode") +BusType = mipi_spi_ns.enum("BusType") + COLOR_ORDERS = { MODE_RGB: ColorOrder.COLOR_ORDER_RGB, MODE_BGR: ColorOrder.COLOR_ORDER_BGR, } COLOR_DEPTHS = { - 8: ColorBitness.COLOR_BITNESS_332, - 16: ColorBitness.COLOR_BITNESS_565, + 8: PixelMode.PIXEL_MODE_8, + 16: PixelMode.PIXEL_MODE_16, + 18: PixelMode.PIXEL_MODE_18, } + DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema +BusTypes = { + TYPE_SINGLE: BusType.BUS_TYPE_SINGLE, + TYPE_QUAD: BusType.BUS_TYPE_QUAD, + TYPE_OCTAL: BusType.BUS_TYPE_OCTAL, +} -DriverChip("CUSTOM", initsequence={}) +DriverChip("CUSTOM") MODELS = DriverChip.models -# These statements are noops, but serve to suppress linting of side-effect-only imports -for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare): +# This loop is a noop, but suppresses linting of side-effect-only imports +for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare, adafruit): pass -PixelMode = mipi_spi_ns.enum("PixelMode") -PIXEL_MODE_18BIT = "18bit" -PIXEL_MODE_16BIT = "16bit" +DISPLAY_18BIT = "18bit" +DISPLAY_16BIT = "16bit" -PIXEL_MODES = { - PIXEL_MODE_16BIT: 0x55, - PIXEL_MODE_18BIT: 0x66, +DISPLAY_PIXEL_MODES = { + DISPLAY_16BIT: (0x55, PixelMode.PIXEL_MODE_16), + DISPLAY_18BIT: (0x66, PixelMode.PIXEL_MODE_18), } +def get_dimensions(config): + 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] + offset_width = dimensions[CONF_OFFSET_WIDTH] + offset_height = dimensions[CONF_OFFSET_HEIGHT] + return width, height, offset_width, offset_height + (width, height) = dimensions + return width, height, 0, 0 + + # Default dimensions, use model defaults + transform = get_transform(config) + + model = MODELS[config[CONF_MODEL]] + width = model.get_default(CONF_WIDTH) + height = model.get_default(CONF_HEIGHT) + offset_width = model.get_default(CONF_OFFSET_WIDTH, 0) + offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0) + + # 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]: + native_width = model.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2) + offset_width = native_width - width - offset_width + if transform[CONF_MIRROR_Y]: + native_height = model.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: + width, height = height, width + offset_height, offset_width = offset_width, offset_height + return width, height, offset_width, offset_height + + +def denominator(config): + """ + Calculate the best denominator for a buffer size fraction. + The denominator must be a number between 2 and 16 that divides the display height evenly, + and the fraction represented by the denominator must be less than or equal to the given fraction. + :config: The configuration dictionary containing the buffer size fraction and display dimensions + :return: The denominator to use for the buffer size fraction + """ + frac = config.get(CONF_BUFFER_SIZE) + if frac is None or frac > 0.75: + return 1 + height, _width, _offset_width, _offset_height = get_dimensions(config) + try: + return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0) + except StopIteration: + raise cv.Invalid( + f"Buffer size fraction {frac} is not compatible with display height {height}" + ) from StopIteration + + def validate_dimension(rounding): def validator(value): value = cv.positive_int(value) @@ -158,41 +235,50 @@ def dimension_schema(rounding): ) -def model_schema(bus_mode, model: DriverChip, swapsies: bool): +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.get(CONF_BUS_MODE, model.modes[0]) transform = cv.Schema( { cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean, + **swap_xy_schema(model), } ) - if model.get_default(CONF_SWAP_XY, False) == cv.UNDEFINED: - transform = transform.extend( - { - cv.Optional(CONF_SWAP_XY): cv.invalid( - "Axis swapping not supported by this model" - ) - } - ) - else: - transform = transform.extend( - { - cv.Required(CONF_SWAP_XY): cv.boolean, - } - ) # CUSTOM model will need to provide a custom init sequence iseqconf = ( cv.Required(CONF_INIT_SEQUENCE) if model.initsequence is None else cv.Optional(CONF_INIT_SEQUENCE) ) - # Dimensions are optional if the model has a default width and the transform is not overridden + # 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 cv_dimensions = ( - cv.Optional if model.get_default(CONF_WIDTH) and not swapsies else cv.Required + cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required ) - pixel_modes = PIXEL_MODES if bus_mode == TYPE_SINGLE else (PIXEL_MODE_16BIT,) + pixel_modes = DISPLAY_PIXEL_MODES if bus_mode == TYPE_SINGLE else (DISPLAY_16BIT,) color_depth = ( ("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit") ) + other_options = [ + CONF_INVERT_COLORS, + CONF_USE_AXIS_FLIPS, + ] + if bus_mode == TYPE_SINGLE: + other_options.append(CONF_SPI_16) schema = ( display.FULL_DISPLAY_SCHEMA.extend( spi.spi_device_schema( @@ -220,11 +306,13 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum( COLOR_ORDERS, upper=True ), + model.option(CONF_BYTE_ORDER, "big_endian"): cv.one_of( + "big_endian", "little_endian", lower=True + ), model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True), model.option(CONF_DRAW_ROUNDING, 2): power_of_two, - model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any( - cv.one_of(*pixel_modes, lower=True), - cv.int_range(0, 255, min_included=True, max_included=True), + model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of( + *pixel_modes, lower=True ), cv.Optional(CONF_TRANSFORM): transform, cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of( @@ -232,19 +320,12 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): ), cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), iseqconf: cv.ensure_list(map_sequence), + cv.Optional(CONF_BUFFER_SIZE): cv.All( + cv.percentage, cv.Range(0.12, 1.0) + ), } ) - .extend( - { - model.option(x): cv.boolean - for x in [ - CONF_DRAW_FROM_ORIGIN, - CONF_SPI_16, - CONF_INVERT_COLORS, - CONF_USE_AXIS_FLIPS, - ] - } - ) + .extend({model.option(x): cv.boolean for x in other_options}) ) if brightness := model.get_default(CONF_BRIGHTNESS): schema = schema.extend( @@ -259,18 +340,25 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): return schema -def rotation_as_transform(model, config): +def is_rotation_transformable(config): """ 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. """ + model = MODELS[config[CONF_MODEL]] rotation = config.get(CONF_ROTATION, 0) return rotation and ( model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 ) -def config_schema(config): +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 + """ # First get the model and bus mode config = cv.Schema( { @@ -288,29 +376,94 @@ def config_schema(config): extra=ALLOW_EXTRA, )(config) bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) - swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True - config = model_schema(bus_mode, model, swapsies)(config) + config = model_schema(config)(config) # Check for invalid combinations of MADCTL config if init_sequence := config.get(CONF_INIT_SEQUENCE): - if MADCTL in [x[0] for x in init_sequence] and CONF_TRANSFORM in config: + commands = [x[0] for x in init_sequence] + if MADCTL in commands and CONF_TRANSFORM in config: raise cv.Invalid( f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence" ) + if PIXFMT in commands: + raise cv.Invalid( + f"PIXFMT ({PIXFMT:#X}) should not be in the init sequence, it will be set automatically" + ) if bus_mode == TYPE_QUAD and CONF_DC_PIN in config: raise cv.Invalid("DC pin is not supported in quad mode") - if config[CONF_PIXEL_MODE] == PIXEL_MODE_18BIT and bus_mode != TYPE_SINGLE: - raise cv.Invalid("18-bit pixel mode is not supported on a quad or octal bus") if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config: raise cv.Invalid(f"DC pin is required in {bus_mode} mode") + denominator(config) return config -CONFIG_SCHEMA = config_schema +CONFIG_SCHEMA = customise_schema -def get_transform(model, config): - can_transform = rotation_as_transform(model, config) +def requires_buffer(config): + """ + Check if the display configuration requires a buffer. It will do so if any drawing methods are configured. + :param config: + :return: True if a buffer is required, False otherwise + """ + return any( + config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD) + ) + + +def get_color_depth(config): + return int(config[CONF_COLOR_DEPTH].removesuffix("bit")) + + +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 "psram" not in global_config and CONF_BUFFER_SIZE not in config: + if not requires_buffer(config): + return config # No buffer needed, so no need to set a buffer size + # If PSRAM is not enabled, choose a small buffer size by default + if not requires_buffer(config): + # not our problem. + return config + color_depth = get_color_depth(config) + frac = denominator(config) + height, width, _offset_width, _offset_height = get_dimensions(config) + + buffer_size = color_depth // 8 * width * height // frac + # Target a buffer size of 20kB + fraction = 20000.0 / buffer_size + try: + config[CONF_BUFFER_SIZE] = 1.0 / next( + x for x in range(2, 17) if fraction >= 1 / x and height % x == 0 + ) + except StopIteration: + # Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0 + # PSRAM will be needed. + if CORE.is_esp32: + raise cv.Invalid( + "PSRAM is required for this display" + ) from StopIteration + + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +def get_transform(config): + """ + Get the transformation configuration for the display. + :param config: + :return: + """ + model = MODELS[config[CONF_MODEL]] + can_transform = is_rotation_transformable(config) transform = config.get( CONF_TRANSFORM, { @@ -350,16 +503,13 @@ def get_sequence(model, config): sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] commands = [x[0] for x in sequence] # Set pixel format if not already in the custom sequence - if PIXFMT not in commands: - pixel_mode = config[CONF_PIXEL_MODE] - if not isinstance(pixel_mode, int): - pixel_mode = PIXEL_MODES[pixel_mode] - sequence.append((PIXFMT, pixel_mode)) + pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]] + sequence.append((PIXFMT, pixel_mode[0])) # Does the chip use the flipping bits for mirroring rather than the reverse order bits? use_flip = config[CONF_USE_AXIS_FLIPS] if MADCTL not in commands: madctl = 0 - transform = get_transform(model, config) + transform = get_transform(config) if transform.get(CONF_TRANSFORM): LOGGER.info("Using hardware transform to implement rotation") if transform.get(CONF_MIRROR_X): @@ -396,63 +546,62 @@ def get_sequence(model, config): ) +def get_instance(config): + """ + Get the type of MipiSpi instance to create based on the configuration, + and the template arguments. + :param config: + :return: type, template arguments + """ + width, height, offset_width, offset_height = get_dimensions(config) + + color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit")) + bufferpixels = COLOR_DEPTHS[color_depth] + + display_pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]][1] + bus_type = config[CONF_BUS_MODE] + if bus_type == TYPE_SINGLE and config.get(CONF_SPI_16, False): + # If the bus mode is single and spi_16 is set, use single 16-bit mode + bus_type = BusType.BUS_TYPE_SINGLE_16 + else: + bus_type = BusTypes[bus_type] + buffer_type = cg.uint8 if color_depth == 8 else cg.uint16 + frac = denominator(config) + rotation = DISPLAY_ROTATIONS[ + 0 if is_rotation_transformable(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) + return MipiSpiBuffer, templateargs + return MipiSpi, templateargs + + async def to_code(config): model = MODELS[config[CONF_MODEL]] - transform = get_transform(model, config) - 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] - offset_width = dimensions[CONF_OFFSET_WIDTH] - offset_height = dimensions[CONF_OFFSET_HEIGHT] - else: - (width, height) = dimensions - offset_width = 0 - offset_height = 0 - else: - # Default dimensions, use model defaults and transform if needed - width = model.get_default(CONF_WIDTH) - height = model.get_default(CONF_HEIGHT) - offset_width = model.get_default(CONF_OFFSET_WIDTH, 0) - offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0) - - # 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]: - native_width = model.get_default( - CONF_NATIVE_WIDTH, width + offset_width * 2 - ) - offset_width = native_width - width - offset_width - if transform[CONF_MIRROR_Y]: - native_height = model.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: - width, height = height, width - offset_height, offset_width = offset_width, offset_height - - color_depth = config[CONF_COLOR_DEPTH] - if color_depth.endswith("bit"): - color_depth = color_depth[:-3] - color_depth = COLOR_DEPTHS[int(color_depth)] - - var = cg.new_Pvariable( - config[CONF_ID], width, height, offset_width, offset_height, color_depth - ) + var_id = config[CONF_ID] + var_id.type, templateargs = get_instance(config) + var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) cg.add(var.set_init_sequence(get_sequence(model, config))) - if rotation_as_transform(model, config): + if is_rotation_transformable(config): if CONF_TRANSFORM in config: LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") else: config[CONF_ROTATION] = 0 cg.add(var.set_model(config[CONF_MODEL])) - cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN])) cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING])) - cg.add(var.set_spi_16(config[CONF_SPI_16])) 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)) @@ -472,4 +621,5 @@ async def to_code(config): cg.add(var.set_writer(lambda_)) await display.register_display(var, config) await spi.register_spi_device(var, config) + # Displays are write-only, set the SPI device to write-only as well cg.add(var.set_write_only(True)) diff --git a/esphome/components/mipi_spi/mipi_spi.cpp b/esphome/components/mipi_spi/mipi_spi.cpp index 962575477d..272915b4e1 100644 --- a/esphome/components/mipi_spi/mipi_spi.cpp +++ b/esphome/components/mipi_spi/mipi_spi.cpp @@ -2,489 +2,5 @@ #include "esphome/core/log.h" namespace esphome { -namespace mipi_spi { - -void MipiSpi::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->spi_setup(); - if (this->dc_pin_ != nullptr) { - this->dc_pin_->setup(); - this->dc_pin_->digital_write(false); - } - for (auto *pin : this->enable_pins_) { - pin->setup(); - pin->digital_write(true); - } - 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); - } - this->bus_width_ = this->parent_->get_bus_width(); - - // need to know when the display is ready for SLPOUT command - will be 120ms after reset - auto when = millis() + 120; - delay(10); - size_t index = 0; - auto &vec = this->init_sequence_; - while (index != vec.size()) { - if (vec.size() - index < 2) { - ESP_LOGE(TAG, "Malformed init sequence"); - this->mark_failed(); - 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) { - ESP_LOGE(TAG, "Malformed init sequence"); - this->mark_failed(); - return; - } - auto arg_byte = vec[index]; - switch (cmd) { - case SLEEP_OUT: { - // are we ready, boots? - int duration = when - millis(); - if (duration > 0) { - ESP_LOGD(TAG, "Sleep %dms", duration); - delay(duration); - } - } break; - - case INVERT_ON: - this->invert_colors_ = true; - break; - case MADCTL_CMD: - this->madctl_ = arg_byte; - break; - case PIXFMT: - this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18; - break; - case BRIGHTNESS: - this->brightness_ = arg_byte; - break; - - default: - break; - } - const auto *ptr = vec.data() + index; - ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte); - this->write_command_(cmd, ptr, num_args); - index += num_args; - if (cmd == SLEEP_OUT) - delay(10); - } - } - this->setup_complete_ = true; - if (this->draw_from_origin_) - check_buffer_(); - ESP_LOGCONFIG(TAG, "MIPI SPI setup complete"); -} - -void MipiSpi::update() { - if (!this->setup_complete_ || this->is_failed()) { - return; - } - this->do_update_(); - 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_); - // 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; - if (this->draw_from_origin_) { - this->x_low_ = 0; - this->y_low_ = 0; - this->x_high_ = this->width_ - 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->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 MipiSpi::fill(Color color) { - if (!this->check_buffer_()) - return; - this->x_low_ = 0; - this->y_low_ = 0; - this->x_high_ = this->get_width_internal() - 1; - this->y_high_ = this->get_height_internal() - 1; - switch (this->color_depth_) { - case display::COLOR_BITNESS_332: { - auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); - memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_); - break; - } - default: { - auto new_color = display::ColorUtil::color_to_565(color); - if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) { - // Upper and lower is equal can use quicker memset operation. Takes ~20ms. - memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_); - } else { - auto *ptr_16 = reinterpret_cast(this->buffer_); - auto len = this->buffer_bytes_ / 2; - while (len--) { - *ptr_16++ = new_color; - } - } - } - } -} - -void MipiSpi::draw_absolute_pixel_internal(int x, int y, Color color) { - 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; - switch (this->color_depth_) { - case display::COLOR_BITNESS_332: { - uint8_t new_color = display::ColorUtil::color_to_332(color); - if (this->buffer_[pos] == new_color) - return; - this->buffer_[pos] = new_color; - break; - } - - case display::COLOR_BITNESS_565: { - 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 = hi_byte | (lo_byte << 8); // big endian - if (ptr_16[pos] == new_color) - return; - ptr_16[pos] = new_color; - break; - } - default: - return; - } - // 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 MipiSpi::reset_params_() { - if (!this->is_ready()) - return; - this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); - if (this->brightness_.has_value()) - this->write_command_(BRIGHTNESS, this->brightness_.value()); -} - -void MipiSpi::write_init_sequence_() { - size_t index = 0; - auto &vec = this->init_sequence_; - while (index != vec.size()) { - if (vec.size() - index < 2) { - ESP_LOGE(TAG, "Malformed init sequence"); - this->mark_failed(); - return; - } - uint8_t cmd = vec[index++]; - uint8_t x = vec[index++]; - if (x == DELAY_FLAG) { - ESP_LOGV(TAG, "Delay %dms", cmd); - delay(cmd); - } else { - uint8_t num_args = x & 0x7F; - if (vec.size() - index < num_args) { - ESP_LOGE(TAG, "Malformed init sequence"); - this->mark_failed(); - return; - } - const auto *ptr = vec.data() + index; - this->write_command_(cmd, ptr, num_args); - index += num_args; - } - } - this->setup_complete_ = true; - ESP_LOGCONFIG(TAG, "MIPI SPI setup complete"); -} - -void MipiSpi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { - ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2); - uint8_t buf[4]; - x1 += this->offset_width_; - x2 += this->offset_width_; - y1 += this->offset_height_; - y2 += this->offset_height_; - put16_be(buf, y1); - put16_be(buf + 2, y2); - this->write_command_(RASET, buf, sizeof buf); - put16_be(buf, x1); - put16_be(buf + 2, x2); - this->write_command_(CASET, buf, sizeof buf); -} - -void MipiSpi::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 (!this->setup_complete_ || this->is_failed()) - return; - if (w <= 0 || h <= 0) - return; - if (bitness != this->color_depth_ || big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) { - Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); - return; - } - if (this->draw_from_origin_) { - auto stride = x_offset + w + x_pad; - for (int y = 0; y != h; y++) { - memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2, - ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2); - } - ptr = this->buffer_; - w = this->width_; - h += y_start; - x_start = 0; - y_start = 0; - x_offset = 0; - y_offset = 0; - } - this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); -} - -void MipiSpi::write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride) { - stride -= w; - uint8_t transfer_buffer[6 * 256]; - size_t idx = 0; // index into transfer_buffer - while (h-- != 0) { - for (auto x = w; x-- != 0;) { - auto color_val = *ptr++; - // deal with byte swapping - transfer_buffer[idx++] = (color_val & 0xF8); // Blue - transfer_buffer[idx++] = ((color_val & 0x7) << 5) | ((color_val & 0xE000) >> 11); // Green - transfer_buffer[idx++] = (color_val >> 5) & 0xF8; // Red - if (idx == sizeof(transfer_buffer)) { - this->write_array(transfer_buffer, idx); - idx = 0; - } - } - ptr += stride; - } - if (idx != 0) - this->write_array(transfer_buffer, idx); -} - -void MipiSpi::write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) { - stride -= w; - uint8_t transfer_buffer[6 * 256]; - size_t idx = 0; // index into transfer_buffer - while (h-- != 0) { - for (auto x = w; x-- != 0;) { - auto color_val = *ptr++; - transfer_buffer[idx++] = color_val & 0xE0; // Red - transfer_buffer[idx++] = (color_val << 3) & 0xE0; // Green - transfer_buffer[idx++] = color_val << 6; // Blue - if (idx == sizeof(transfer_buffer)) { - this->write_array(transfer_buffer, idx); - idx = 0; - } - } - ptr += stride; - } - if (idx != 0) - this->write_array(transfer_buffer, idx); -} - -void MipiSpi::write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) { - stride -= w; - uint8_t transfer_buffer[6 * 256]; - size_t idx = 0; // index into transfer_buffer - while (h-- != 0) { - for (auto x = w; x-- != 0;) { - auto color_val = *ptr++; - transfer_buffer[idx++] = (color_val & 0xE0) | ((color_val & 0x1C) >> 2); - transfer_buffer[idx++] = (color_val & 0x3) << 3; - if (idx == sizeof(transfer_buffer)) { - this->write_array(transfer_buffer, idx); - idx = 0; - } - } - ptr += stride; - } - if (idx != 0) - this->write_array(transfer_buffer, idx); -} - -void MipiSpi::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) { - this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1); - auto stride = x_offset + w + x_pad; - const auto *offset_ptr = ptr; - if (this->color_depth_ == display::COLOR_BITNESS_332) { - offset_ptr += y_offset * stride + x_offset; - } else { - stride *= 2; - offset_ptr += y_offset * stride + x_offset * 2; - } - - switch (this->bus_width_) { - case 4: - this->enable(); - if (x_offset == 0 && x_pad == 0 && y_offset == 0) { - // 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_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h * 2, 4); - } else { - this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, nullptr, 0, 4); - for (int y = 0; y != h; y++) { - this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 4); - offset_ptr += stride; - } - } - break; - - case 8: - this->write_command_(WDATA); - this->enable(); - if (x_offset == 0 && x_pad == 0 && y_offset == 0) { - this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h * 2, 8); - } else { - for (int y = 0; y != h; y++) { - this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 8); - offset_ptr += stride; - } - } - break; - - default: - this->write_command_(WDATA); - this->enable(); - - if (this->color_depth_ == display::COLOR_BITNESS_565) { - // Source buffer is 16-bit RGB565 - if (this->pixel_mode_ == PIXEL_MODE_18) { - // Convert RGB565 to RGB666 - this->write_18_from_16_bit_(reinterpret_cast(offset_ptr), w, h, stride / 2); - } else { - // Direct RGB565 output - if (x_offset == 0 && x_pad == 0 && y_offset == 0) { - this->write_array(ptr, w * h * 2); - } else { - for (int y = 0; y != h; y++) { - this->write_array(offset_ptr, w * 2); - offset_ptr += stride; - } - } - } - } else { - // Source buffer is 8-bit RGB332 - if (this->pixel_mode_ == PIXEL_MODE_18) { - // Convert RGB332 to RGB666 - this->write_18_from_8_bit_(offset_ptr, w, h, stride); - } else { - this->write_16_from_8_bit_(offset_ptr, w, h, stride); - } - break; - } - } - this->disable(); -} - -void MipiSpi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { - ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str()); - if (this->bus_width_ == 4) { - this->enable(); - this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); - this->disable(); - } else if (this->bus_width_ == 8) { - this->dc_pin_->digital_write(false); - this->enable(); - this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8); - this->disable(); - this->dc_pin_->digital_write(true); - if (len != 0) { - this->enable(); - this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8); - this->disable(); - } - } else { - this->dc_pin_->digital_write(false); - this->enable(); - this->write_byte(cmd); - this->disable(); - this->dc_pin_->digital_write(true); - if (len != 0) { - if (this->spi_16_) { - for (size_t i = 0; i != len; i++) { - this->enable(); - this->write_byte(0); - this->write_byte(bytes[i]); - this->disable(); - } - } else { - this->enable(); - this->write_array(bytes, len); - this->disable(); - } - } - } -} - -void MipiSpi::dump_config() { - ESP_LOGCONFIG(TAG, - "MIPI_SPI Display\n" - " Model: %s\n" - " Width: %u\n" - " Height: %u", - this->model_, this->width_, this->height_); - if (this->offset_width_ != 0) - ESP_LOGCONFIG(TAG, " Offset width: %u", this->offset_width_); - if (this->offset_height_ != 0) - ESP_LOGCONFIG(TAG, " Offset height: %u", this->offset_height_); - ESP_LOGCONFIG(TAG, - " Swap X/Y: %s\n" - " Mirror X: %s\n" - " Mirror Y: %s\n" - " Color depth: %d bits\n" - " Invert colors: %s\n" - " Color order: %s\n" - " Pixel mode: %s", - YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)), - YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), - this->color_depth_ == display::COLOR_BITNESS_565 ? 16 : 8, YESNO(this->invert_colors_), - this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", this->pixel_mode_ == PIXEL_MODE_18 ? "18bit" : "16bit"); - if (this->brightness_.has_value()) - ESP_LOGCONFIG(TAG, " Brightness: %u", this->brightness_.value()); - if (this->spi_16_) - ESP_LOGCONFIG(TAG, " SPI 16bit: YES"); - ESP_LOGCONFIG(TAG, " Draw rounding: %u", this->draw_rounding_); - if (this->draw_from_origin_) - ESP_LOGCONFIG(TAG, " Draw from origin: YES"); - LOG_PIN(" CS Pin: ", this->cs_); - LOG_PIN(" Reset Pin: ", this->reset_pin_); - LOG_PIN(" DC Pin: ", this->dc_pin_); - ESP_LOGCONFIG(TAG, - " SPI Mode: %d\n" - " SPI Data rate: %dMHz\n" - " SPI Bus width: %d", - this->mode_, static_cast(this->data_rate_ / 1000000), this->bus_width_); -} - -} // namespace mipi_spi +namespace mipi_spi {} // namespace mipi_spi } // namespace esphome diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 052ebe3a6b..cdba5a3235 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -4,40 +4,39 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/display/display.h" -#include "esphome/components/display/display_buffer.h" #include "esphome/components/display/display_color_utils.h" namespace esphome { namespace mipi_spi { constexpr static const char *const TAG = "display.mipi_spi"; -static const uint8_t SW_RESET_CMD = 0x01; -static const uint8_t SLEEP_OUT = 0x11; -static const uint8_t NORON = 0x13; -static const uint8_t INVERT_OFF = 0x20; -static const uint8_t INVERT_ON = 0x21; -static const uint8_t ALL_ON = 0x23; -static const uint8_t WRAM = 0x24; -static const uint8_t MIPI = 0x26; -static const uint8_t DISPLAY_ON = 0x29; -static const uint8_t RASET = 0x2B; -static const uint8_t CASET = 0x2A; -static const uint8_t WDATA = 0x2C; -static const uint8_t TEON = 0x35; -static const uint8_t MADCTL_CMD = 0x36; -static const uint8_t PIXFMT = 0x3A; -static const uint8_t BRIGHTNESS = 0x51; -static const uint8_t SWIRE1 = 0x5A; -static const uint8_t SWIRE2 = 0x5B; -static const uint8_t PAGESEL = 0xFE; +static constexpr uint8_t SW_RESET_CMD = 0x01; +static constexpr uint8_t SLEEP_OUT = 0x11; +static constexpr uint8_t NORON = 0x13; +static constexpr uint8_t INVERT_OFF = 0x20; +static constexpr uint8_t INVERT_ON = 0x21; +static constexpr uint8_t ALL_ON = 0x23; +static constexpr uint8_t WRAM = 0x24; +static constexpr uint8_t MIPI = 0x26; +static constexpr uint8_t DISPLAY_ON = 0x29; +static constexpr uint8_t RASET = 0x2B; +static constexpr uint8_t CASET = 0x2A; +static constexpr uint8_t WDATA = 0x2C; +static constexpr uint8_t TEON = 0x35; +static constexpr uint8_t MADCTL_CMD = 0x36; +static constexpr uint8_t PIXFMT = 0x3A; +static constexpr uint8_t BRIGHTNESS = 0x51; +static constexpr uint8_t SWIRE1 = 0x5A; +static constexpr uint8_t SWIRE2 = 0x5B; +static constexpr uint8_t PAGESEL = 0xFE; -static const uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top -static const uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left -static const uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes -static const uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order -static const uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order -static const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally -static const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically +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_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order +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 static const uint8_t DELAY_FLAG = 0xFF; // store a 16 bit value in a buffer, big endian. @@ -46,28 +45,44 @@ static inline void put16_be(uint8_t *buf, uint16_t value) { buf[1] = value; } +// Buffer mode, conveniently also the number of bytes in a pixel enum PixelMode { - PIXEL_MODE_16, - PIXEL_MODE_18, + PIXEL_MODE_8 = 1, + PIXEL_MODE_16 = 2, + PIXEL_MODE_18 = 3, }; -class MipiSpi : public display::DisplayBuffer, +enum BusType { + BUS_TYPE_SINGLE = 1, + BUS_TYPE_QUAD = 4, + BUS_TYPE_OCTAL = 8, + BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer +}; + +/** + * Base class for MIPI SPI displays. + * All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file. + * + * @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t + * @tparam BUFFERPIXEL Color depth of the buffer + * @tparam DISPLAYPIXEL Color depth of the display + * @tparam BUS_TYPE The type of the interface bus (single, quad, octal) + * @tparam WIDTH Width of the display in pixels + * @tparam HEIGHT Height of the display in pixels + * @tparam OFFSET_WIDTH The x-offset of the display in pixels + * @tparam OFFSET_HEIGHT The y-offset of the display in pixels + * buffer + */ +template +class MipiSpi : public display::Display, public spi::SPIDevice { public: - MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth) - : width_(width), - height_(height), - offset_width_(offset_width), - offset_height_(offset_height), - color_depth_(color_depth) {} + MipiSpi() {} + 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; } - void update() override; - void setup() override; - display::ColorOrder get_color_mode() { - return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB; - } - void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_enable_pins(std::vector enable_pins) { this->enable_pins_ = std::move(enable_pins); } void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } @@ -79,93 +94,524 @@ class MipiSpi : public display::DisplayBuffer, this->brightness_ = brightness; this->reset_params_(); } - - void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; } display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } - void dump_config() override; - int get_width_internal() override { return this->width_; } - int get_height_internal() override { return this->height_; } - bool can_proceed() override { return this->setup_complete_; } + 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; } - void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; } + + // reset the display, and write the init sequence + void setup() override { + this->spi_setup(); + if (this->dc_pin_ != nullptr) { + this->dc_pin_->setup(); + this->dc_pin_->digital_write(false); + } + for (auto *pin : this->enable_pins_) { + pin->setup(); + pin->digital_write(true); + } + 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); + } + + // need to know when the display is ready for SLPOUT command - will be 120ms after reset + auto when = millis() + 120; + delay(10); + size_t index = 0; + auto &vec = this->init_sequence_; + while (index != vec.size()) { + if (vec.size() - index < 2) { + esph_log_e(TAG, "Malformed init sequence"); + this->mark_failed(); + return; + } + uint8_t cmd = vec[index++]; + uint8_t x = vec[index++]; + if (x == DELAY_FLAG) { + esph_log_d(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + uint8_t num_args = x & 0x7F; + if (vec.size() - index < num_args) { + esph_log_e(TAG, "Malformed init sequence"); + this->mark_failed(); + return; + } + auto arg_byte = vec[index]; + switch (cmd) { + case SLEEP_OUT: { + // are we ready, boots? + int duration = when - millis(); + if (duration > 0) { + esph_log_d(TAG, "Sleep %dms", duration); + delay(duration); + } + } break; + + case INVERT_ON: + this->invert_colors_ = true; + break; + case MADCTL_CMD: + this->madctl_ = arg_byte; + break; + case BRIGHTNESS: + this->brightness_ = arg_byte; + break; + + default: + break; + } + const auto *ptr = vec.data() + index; + esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte); + this->write_command_(cmd, ptr, num_args); + index += num_args; + if (cmd == SLEEP_OUT) + delay(10); + } + } + // init sequence no longer needed + this->init_sequence_.clear(); + } + + // Drawing operations + + 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 { + if (this->is_failed()) + return; + if (w <= 0 || h <= 0) + return; + if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN) { + // note that the usual logging macros are banned in header files, so use their replacement + esph_log_e(TAG, "Unsupported color depth or bit order"); + return; + } + this->write_to_display_(x_start, y_start, w, h, reinterpret_cast(ptr), x_offset, y_offset, + x_pad); + } + + void dump_config() override { + esph_log_config(TAG, + "MIPI_SPI Display\n" + " Model: %s\n" + " Width: %u\n" + " Height: %u", + this->model_, WIDTH, HEIGHT); + if constexpr (OFFSET_WIDTH != 0) + esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH); + if constexpr (OFFSET_HEIGHT != 0) + esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT); + esph_log_config(TAG, + " Swap X/Y: %s\n" + " Mirror X: %s\n" + " Mirror Y: %s\n" + " Invert colors: %s\n" + " Color order: %s\n" + " Display pixels: %d bits\n" + " Endianness: %s\n", + YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)), + YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_), + this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little"); + if (this->brightness_.has_value()) + esph_log_config(TAG, " Brightness: %u", this->brightness_.value()); + if (this->cs_ != nullptr) + esph_log_config(TAG, " CS Pin: %s", this->cs_->dump_summary().c_str()); + if (this->reset_pin_ != nullptr) + esph_log_config(TAG, " Reset Pin: %s", this->reset_pin_->dump_summary().c_str()); + if (this->dc_pin_ != nullptr) + esph_log_config(TAG, " DC Pin: %s", this->dc_pin_->dump_summary().c_str()); + esph_log_config(TAG, + " SPI Mode: %d\n" + " SPI Data rate: %dMHz\n" + " SPI Bus width: %d", + this->mode_, static_cast(this->data_rate_ / 1000000), BUS_TYPE); + } protected: - bool check_buffer_() { - if (this->is_failed()) - return false; - if (this->buffer_ != nullptr) - return true; - auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1; - this->init_internal_(this->width_ * this->height_ * bytes_per_pixel); - if (this->buffer_ == nullptr) { - this->mark_failed(); - return false; - } - this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel; - return true; - } - void fill(Color color) override; - void draw_absolute_pixel_internal(int x, int y, Color color) override; - 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_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride); - void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride); - void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride); - 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); - /** - * the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the - * sample code.) - * - * Immediately after enabling /CS send 4 bytes in single-dataline SPI mode: - * 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be - * sent in 1-dataline SPI. The second indicates quad mode. - * 1: 0x00 - * 2: The command (register address) byte. - * 3: 0x00 - * - * This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte. - * At the conclusion of the write, de-assert /CS. - * - * @param cmd - * @param bytes - * @param len - */ - void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len); - + /* METHODS */ + // convenience functions to write commands with or without data void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); } void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); } - void reset_params_(); - void write_init_sequence_(); - void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2); + // Writes a command to the display, with the given bytes. + void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { + esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str()); + if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { + this->enable(); + this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); + this->disable(); + } else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) { + this->dc_pin_->digital_write(false); + this->enable(); + this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8); + this->disable(); + this->dc_pin_->digital_write(true); + if (len != 0) { + this->enable(); + this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8); + this->disable(); + } + } else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE) { + this->dc_pin_->digital_write(false); + this->enable(); + this->write_byte(cmd); + this->disable(); + this->dc_pin_->digital_write(true); + if (len != 0) { + this->enable(); + this->write_array(bytes, len); + this->disable(); + } + } else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE_16) { + this->dc_pin_->digital_write(false); + this->enable(); + this->write_byte(cmd); + this->disable(); + this->dc_pin_->digital_write(true); + for (size_t i = 0; i != len; i++) { + this->enable(); + this->write_byte(0); + this->write_byte(bytes[i]); + this->disable(); + } + } + } + + // write changed parameters to the display + void reset_params_() { + if (!this->is_ready()) + return; + this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); + if (this->brightness_.has_value()) + this->write_command_(BRIGHTNESS, this->brightness_.value()); + } + + // set the address window for the next data write + void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { + esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2); + uint8_t buf[4]; + x1 += OFFSET_WIDTH; + x2 += OFFSET_WIDTH; + y1 += OFFSET_HEIGHT; + y2 += OFFSET_HEIGHT; + put16_be(buf, y1); + put16_be(buf + 2, y2); + this->write_command_(RASET, buf, sizeof buf); + put16_be(buf, x1); + put16_be(buf + 2, x2); + this->write_command_(CASET, buf, sizeof buf); + if constexpr (BUS_TYPE != BUS_TYPE_QUAD) { + this->write_command_(WDATA); + } + } + + // map the display color bitness to the pixel mode + static PixelMode get_pixel_mode(display::ColorBitness bitness) { + switch (bitness) { + case display::COLOR_BITNESS_888: + return PIXEL_MODE_18; // 18 bits per pixel + case display::COLOR_BITNESS_565: + return PIXEL_MODE_16; // 16 bits per pixel + default: + return PIXEL_MODE_8; // Default to 8 bits per pixel + } + } + + /** + * Writes a buffer to the display. + * @param w Width of each line in bytes + * @param h Height of the buffer in rows + * @param pad Padding in bytes after each line + */ + void write_display_data_(const uint8_t *ptr, size_t w, size_t h, size_t pad) { + if (pad == 0) { + if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) { + this->write_array(ptr, w * h); + } else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { + this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h, 4); + } else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) { + this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8); + } + } else { + for (size_t y = 0; y != 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) { + this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w, 4); + } else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) { + this->write_cmd_addr_data(0, 0, 0, 0, ptr, w, 8); + } + ptr += w + pad; + } + } + } + + /** + * Writes a buffer to the display. + * + * The ptr is a pointer to the pixel data + * The other parameters are all in pixel units. + */ + void write_to_display_(int x_start, int y_start, int w, int h, const BUFFERTYPE *ptr, int x_offset, int y_offset, + int x_pad) { + this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1); + this->enable(); + ptr += y_offset * (x_offset + w + x_pad) + x_offset; + if constexpr (BUFFERPIXEL == DISPLAYPIXEL) { + this->write_display_data_(reinterpret_cast(ptr), w * sizeof(BUFFERTYPE), h, + x_pad * sizeof(BUFFERTYPE)); + } else { + // type conversion required, do it in chunks + 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++) { + auto color_val = ptr[y * stride + x]; + if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) { + // 16 to 18 bit conversion + if constexpr (IS_BIG_ENDIAN) { + *dptr++ = color_val & 0xF8; + *dptr++ = ((color_val & 0x7) << 5) | (color_val & 0xE000) >> 11; + *dptr++ = (color_val >> 5) & 0xF8; + } else { + *dptr++ = (color_val >> 8) & 0xF8; // Blue + *dptr++ = (color_val & 0x7E0) >> 3; + *dptr++ = color_val << 3; + } + } else if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_8) { + // 8 bit to 18 bit conversion + *dptr++ = color_val << 6; // Blue + *dptr++ = (color_val & 0x1C) << 3; // Green + *dptr++ = (color_val & 0xE0); // Red + } else if constexpr (DISPLAYPIXEL == PIXEL_MODE_16 && BUFFERPIXEL == PIXEL_MODE_8) { + if constexpr (IS_BIG_ENDIAN) { + *dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2); + *dptr++ = (color_val & 3) << 3; + } else { + *dptr++ = (color_val & 3) << 3; + *dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2); + } + } + // buffer full? Flush. + if (dptr == dbuffer + sizeof(dbuffer)) { + this->write_display_data_(dbuffer, sizeof(dbuffer), 1, 0); + dptr = dbuffer; + } + } + } + // flush any remaining data + if (dptr != dbuffer) { + this->write_display_data_(dbuffer, dptr - dbuffer, 1, 0); + } + } + this->disable(); + } + + /* PROPERTIES */ + + // GPIO pins GPIOPin *reset_pin_{nullptr}; std::vector enable_pins_{}; GPIOPin *dc_pin_{nullptr}; - uint16_t x_low_{1}; - uint16_t y_low_{1}; - uint16_t x_high_{0}; - uint16_t y_high_{0}; - bool setup_complete_{}; + // other properties set by configuration bool invert_colors_{}; - size_t width_; - size_t height_; - int16_t offset_width_; - int16_t offset_height_; - size_t buffer_bytes_{0}; - display::ColorBitness color_depth_; - PixelMode pixel_mode_{PIXEL_MODE_16}; - uint8_t bus_width_{}; - bool spi_16_{}; - uint8_t madctl_{}; - bool draw_from_origin_{false}; unsigned draw_rounding_{2}; optional brightness_{}; const char *model_{"Unknown"}; std::vector init_sequence_{}; + uint8_t madctl_{}; }; + +/** + * Class for MIPI SPI displays with a buffer. + * + * @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t + * @tparam BUFFERPIXEL Color depth of the buffer + * @tparam DISPLAYPIXEL Color depth of the display + * @tparam BUS_TYPE The type of the interface bus (single, quad, octal) + * @tparam ROTATION The rotation of the display + * @tparam WIDTH Width of the display in pixels + * @tparam HEIGHT Height of the display in pixels + * @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). + */ +template +class MipiSpiBuffer : public MipiSpi { + public: + MipiSpiBuffer() { this->rotation_ = ROTATION; } + + void dump_config() override { + MipiSpi::dump_config(); + esph_log_config(TAG, + " Rotation: %d°\n" + " Buffer pixels: %d bits\n" + " Buffer fraction: 1/%d\n" + " Buffer bytes: %zu\n" + " Draw rounding: %u", + this->rotation_, BUFFERPIXEL * 8, FRACTION, sizeof(BUFFERTYPE) * WIDTH * HEIGHT / FRACTION, + this->draw_rounding_); + } + + void setup() override { + MipiSpi::setup(); + RAMAllocator allocator{}; + this->buffer_ = allocator.allocate(WIDTH * HEIGHT / FRACTION); + if (this->buffer_ == nullptr) { + this->mark_failed("Buffer allocation failed"); + } + } + + void update() override { +#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE + auto now = millis(); +#endif + if (this->is_failed()) { + return; + } + // for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of + // the display height, + for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) { +#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE + auto lap = millis(); +#endif + this->end_line_ = this->start_line_ + HEIGHT / FRACTION; + if (this->auto_clear_enabled_) { + this->clear(); + } + if (this->page_ != nullptr) { + this->page_->get_writer()(*this); + } else if (this->writer_.has_value()) { + (*this->writer_)(*this); + } else { + this->test_card(); + } +#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE + esph_log_v(TAG, "Drawing from line %d took %dms", this->start_line_, millis() - lap); + lap = millis(); +#endif + if (this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) + return; + esph_log_v(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_); + // 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; + 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); + // invalidate watermarks + this->x_low_ = WIDTH; + this->y_low_ = HEIGHT; + this->x_high_ = 0; + this->y_high_ = 0; +#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE + esph_log_v(TAG, "Write to display took %dms", millis() - lap); + lap = millis(); +#endif + } +#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE + esph_log_v(TAG, "Total update took %dms", millis() - now); +#endif + } + + // Draw a pixel at the given coordinates. + void draw_pixel_at(int x, int y, Color color) override { + 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); + if (x < this->x_low_) { + this->x_low_ = x; + } + if (x > this->x_high_) { + this->x_high_ = x; + } + if (y < this->y_low_) { + this->y_low_ = y; + } + if (y > this->y_high_) { + this->y_high_ = y; + } + } + + // Fills the display with a color. + void fill(Color color) override { + this->x_low_ = 0; + this->y_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)); + } + + int get_width() override { + if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES) + return HEIGHT; + return WIDTH; + } + + int get_height() override { + if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES) + return WIDTH; + return HEIGHT; + } + + protected: + // Rotate the coordinates to match the display orientation. + void rotate_coordinates_(int &x, int &y) const { + if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) { + x = WIDTH - x - 1; + y = HEIGHT - y - 1; + } else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) { + auto tmp = x; + x = WIDTH - y - 1; + y = tmp; + } else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) { + auto tmp = y; + y = HEIGHT - x - 1; + x = tmp; + } + } + + // Convert a color to the buffer pixel format. + BUFFERTYPE convert_color_(Color &color) const { + if constexpr (BUFFERPIXEL == PIXEL_MODE_8) { + return (color.red & 0xE0) | (color.g & 0xE0) >> 3 | color.b >> 6; + } else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) { + if constexpr (IS_BIG_ENDIAN) { + return (color.r & 0xF8) | color.g >> 5 | (color.g & 0x1C) << 11 | (color.b & 0xF8) << 5; + } else { + return (color.r & 0xF8) << 8 | (color.g & 0xFC) << 3 | color.b >> 3; + } + } + return static_cast(0); + } + + BUFFERTYPE *buffer_{}; + uint16_t x_low_{WIDTH}; + uint16_t y_low_{HEIGHT}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; + uint16_t start_line_{0}; + uint16_t end_line_{1}; +}; + } // namespace mipi_spi } // namespace esphome diff --git a/esphome/components/mipi_spi/models/adafruit.py b/esphome/components/mipi_spi/models/adafruit.py new file mode 100644 index 0000000000..0e91107bee --- /dev/null +++ b/esphome/components/mipi_spi/models/adafruit.py @@ -0,0 +1,30 @@ +from .ili import ST7789V + +ST7789V.extend( + "ADAFRUIT-FUNHOUSE", + height=240, + width=240, + offset_height=0, + offset_width=0, + cs_pin=40, + dc_pin=39, + reset_pin=41, + invert_colors=True, + mirror_x=True, + mirror_y=True, + data_rate="80MHz", +) + +ST7789V.extend( + "ADAFRUIT-S2-TFT-FEATHER", + height=240, + width=135, + offset_height=52, + offset_width=40, + cs_pin=7, + dc_pin=39, + reset_pin=40, + invert_colors=True, +) + +models = {} diff --git a/esphome/components/mipi_spi/models/amoled.py b/esphome/components/mipi_spi/models/amoled.py index 14277b243f..882d19db30 100644 --- a/esphome/components/mipi_spi/models/amoled.py +++ b/esphome/components/mipi_spi/models/amoled.py @@ -67,6 +67,14 @@ RM690B0 = DriverChip( ), ) -T4_S3_AMOLED = RM690B0.extend("T4-S3", width=450, offset_width=16, bus_mode=TYPE_QUAD) +T4_S3_AMOLED = RM690B0.extend( + "T4-S3", + width=450, + offset_width=16, + cs_pin=11, + reset_pin=13, + enable_pin=9, + bus_mode=TYPE_QUAD, +) models = {} diff --git a/esphome/components/mipi_spi/models/waveshare.py b/esphome/components/mipi_spi/models/waveshare.py index 6d14f56fc6..726718aaf6 100644 --- a/esphome/components/mipi_spi/models/waveshare.py +++ b/esphome/components/mipi_spi/models/waveshare.py @@ -1,3 +1,5 @@ +import esphome.config_validation as cv + from . import DriverChip from .ili import ILI9488_A @@ -128,6 +130,7 @@ DriverChip( ILI9488_A.extend( "PICO-RESTOUCH-LCD-3.5", + swap_xy=cv.UNDEFINED, spi_16=True, pixel_mode="16bit", mirror_x=True, diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 5b93789447..f3e57a66be 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -193,13 +193,17 @@ void MQTTClientComponent::start_dnslookup_() { this->dns_resolve_error_ = false; this->dns_resolved_ = false; ip_addr_t addr; + err_t err; + { + LwIPLock lock; #if USE_NETWORK_IPV6 - err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, - MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV6_IPV4); + err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback, + this, LWIP_DNS_ADDRTYPE_IPV6_IPV4); #else - err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, - MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV4); + err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback, + this, LWIP_DNS_ADDRTYPE_IPV4); #endif /* USE_NETWORK_IPV6 */ + } switch (err) { case ERR_OK: { // Got IP immediately diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index b51f4d903e..6ceaf219ff 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -129,21 +129,16 @@ bool MQTTComponent::send_discovery_() { root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available; } - std::string unique_id = this->unique_id(); const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); - if (!unique_id.empty()) { - root[MQTT_UNIQUE_ID] = unique_id; + if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { + char friendly_name_hash[9]; + sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name())); + friendly_name_hash[8] = 0; // ensure the hash-string ends with null + root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; } else { - if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { - char friendly_name_hash[9]; - sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name())); - friendly_name_hash[8] = 0; // ensure the hash-string ends with null - root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; - } else { - // default to almost-unique ID. It's a hack but the only way to get that - // gorgeous device registry view. - root[MQTT_UNIQUE_ID] = "ESP" + this->component_type() + this->get_default_object_id_(); - } + // default to almost-unique ID. It's a hack but the only way to get that + // gorgeous device registry view. + root[MQTT_UNIQUE_ID] = "ESP" + this->component_type() + this->get_default_object_id_(); } const std::string &node_name = App.get_name(); @@ -286,7 +281,6 @@ void MQTTComponent::call_dump_config() { this->dump_config(); } void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } -std::string MQTTComponent::unique_id() { return ""; } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } // Pull these properties from EntityBase if not overridden diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 01ba98ad40..851fdd842c 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -164,13 +164,6 @@ class MQTTComponent : public Component { */ virtual const EntityBase *get_entity() const = 0; - /** A unique ID for this MQTT component, empty for no unique id. See unique ID requirements: - * https://developers.home-assistant.io/docs/en/entity_registry_index.html#unique-id-requirements - * - * @return The unique id as a string. - */ - virtual std::string unique_id(); - /// Get the friendly name of this MQTT component. virtual std::string friendly_name() const; diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 9324ea9bb1..2e1db1908f 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -76,7 +76,6 @@ bool MQTTSensorComponent::publish_state(float value) { int8_t accuracy = this->sensor_->get_accuracy_decimals(); return this->publish(this->get_state_topic_(), value_accuracy_to_string(value, accuracy)); } -std::string MQTTSensorComponent::unique_id() { return this->sensor_->unique_id(); } } // namespace mqtt } // namespace esphome diff --git a/esphome/components/mqtt/mqtt_sensor.h b/esphome/components/mqtt/mqtt_sensor.h index adc201736a..15ea703ad4 100644 --- a/esphome/components/mqtt/mqtt_sensor.h +++ b/esphome/components/mqtt/mqtt_sensor.h @@ -46,7 +46,6 @@ class MQTTSensorComponent : public mqtt::MQTTComponent { /// Override for MQTTComponent, returns "sensor". std::string component_type() const override; const EntityBase *get_entity() const override; - std::string unique_id() override; sensor::Sensor *sensor_; optional expire_after_; // Override the expire after advertised to Home Assistant diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index 0cc5de07a3..42260ed2a8 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -40,7 +40,6 @@ bool MQTTTextSensor::send_initial_state() { } std::string MQTTTextSensor::component_type() const { return "sensor"; } const EntityBase *MQTTTextSensor::get_entity() const { return this->sensor_; } -std::string MQTTTextSensor::unique_id() { return this->sensor_->unique_id(); } } // namespace mqtt } // namespace esphome diff --git a/esphome/components/mqtt/mqtt_text_sensor.h b/esphome/components/mqtt/mqtt_text_sensor.h index fe53a6fefd..9a14efdd16 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.h +++ b/esphome/components/mqtt/mqtt_text_sensor.h @@ -28,7 +28,6 @@ class MQTTTextSensor : public mqtt::MQTTComponent { protected: std::string component_type() const override; const EntityBase *get_entity() const override; - std::string unique_id() override; text_sensor::TextSensor *sensor_; }; diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py new file mode 100644 index 0000000000..c23298e38f --- /dev/null +++ b/esphome/components/nrf52/__init__.py @@ -0,0 +1,218 @@ +from __future__ import annotations + +from pathlib import Path + +import esphome.codegen as cg +from esphome.components.zephyr import ( + copy_files as zephyr_copy_files, + zephyr_add_pm_static, + zephyr_set_core_data, + zephyr_to_code, +) +from esphome.components.zephyr.const import ( + BOOTLOADER_MCUBOOT, + KEY_BOOTLOADER, + KEY_ZEPHYR, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BOARD, + CONF_FRAMEWORK, + KEY_CORE, + KEY_FRAMEWORK_VERSION, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PLATFORM_NRF52, +) +from esphome.core import CORE, EsphomeError, coroutine_with_priority +from esphome.storage_json import StorageJSON +from esphome.types import ConfigType + +from .boards import BOARDS_ZEPHYR, BOOTLOADER_CONFIG +from .const import ( + BOOTLOADER_ADAFRUIT, + BOOTLOADER_ADAFRUIT_NRF52_SD132, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, +) + +# force import gpio to register pin schema +from .gpio import nrf52_pin_to_code # noqa + +CODEOWNERS = ["@tomaszduda23"] +AUTO_LOAD = ["zephyr"] +IS_TARGET_PLATFORM = True + + +def set_core_data(config: ConfigType) -> ConfigType: + zephyr_set_core_data(config) + CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52 + CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(2, 6, 1) + + if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG: + zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]]) + + return config + + +BOOTLOADERS = [ + BOOTLOADER_ADAFRUIT, + BOOTLOADER_ADAFRUIT_NRF52_SD132, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, + BOOTLOADER_MCUBOOT, +] + + +def _detect_bootloader(config: ConfigType) -> ConfigType: + """Detect the bootloader for the given board.""" + config = config.copy() + bootloaders: list[str] = [] + board = config[CONF_BOARD] + + if board in BOARDS_ZEPHYR and KEY_BOOTLOADER in BOARDS_ZEPHYR[board]: + # this board have bootloaders config available + bootloaders = BOARDS_ZEPHYR[board][KEY_BOOTLOADER] + + if KEY_BOOTLOADER not in config: + if bootloaders: + # there is no bootloader in config -> take first one + config[KEY_BOOTLOADER] = bootloaders[0] + else: + # make mcuboot as default if there is no configuration for that board + config[KEY_BOOTLOADER] = BOOTLOADER_MCUBOOT + elif bootloaders and config[KEY_BOOTLOADER] not in bootloaders: + raise cv.Invalid( + f"{board} does not support {config[KEY_BOOTLOADER]}, select one of: {', '.join(bootloaders)}" + ) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_BOARD): cv.string_strict, + cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True), + } + ), + _detect_bootloader, + set_core_data, +) + + +@coroutine_with_priority(1000) +async def to_code(config: ConfigType) -> None: + """Convert the configuration to code.""" + cg.add_platformio_option("board", config[CONF_BOARD]) + cg.add_build_flag("-DUSE_NRF52") + cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) + cg.add_define("ESPHOME_VARIANT", "NRF52") + cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK]) + cg.add_platformio_option( + "platform", + "https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip", + ) + 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", + ], + ) + + if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT: + # make sure that firmware.zip is created + # for Adafruit_nRF52_Bootloader + cg.add_platformio_option("board_upload.protocol", "nrfutil") + cg.add_platformio_option("board_upload.use_1200bps_touch", "true") + cg.add_platformio_option("board_upload.require_upload_port", "true") + cg.add_platformio_option("board_upload.wait_for_upload_port", "true") + + zephyr_to_code(config) + + +def copy_files() -> None: + """Copy files to the build directory.""" + zephyr_copy_files() + + +def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]: + """Get the download types for the firmware.""" + types = [] + UF2_PATH = "zephyr/zephyr.uf2" + DFU_PATH = "firmware.zip" + HEX_PATH = "zephyr/zephyr.hex" + HEX_MERGED_PATH = "zephyr/merged.hex" + APP_IMAGE_PATH = "zephyr/app_update.bin" + build_dir = Path(storage_json.firmware_bin_path).parent + if (build_dir / UF2_PATH).is_file(): + types = [ + { + "title": "UF2 package (recommended)", + "description": "For flashing via Adafruit nRF52 Bootloader as a flash drive.", + "file": UF2_PATH, + "download": f"{storage_json.name}.uf2", + }, + { + "title": "DFU package", + "description": "For flashing via adafruit-nrfutil using USB CDC.", + "file": DFU_PATH, + "download": f"dfu-{storage_json.name}.zip", + }, + ] + else: + types = [ + { + "title": "HEX package", + "description": "For flashing via pyocd using SWD.", + "file": ( + HEX_MERGED_PATH + if (build_dir / HEX_MERGED_PATH).is_file() + else HEX_PATH + ), + "download": f"{storage_json.name}.hex", + }, + ] + if (build_dir / APP_IMAGE_PATH).is_file(): + types += [ + { + "title": "App update package", + "description": "For flashing via mcumgr-web using BLE or smpclient using USB CDC.", + "file": APP_IMAGE_PATH, + "download": f"app-{storage_json.name}.img", + }, + ] + + return types + + +def _upload_using_platformio( + config: ConfigType, port: str, upload_args: list[str] +) -> int | str: + from esphome import platformio_api + + if port is not None: + upload_args += ["--upload-port", port] + return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) + + +def upload_program(config: ConfigType, args, host: str) -> bool: + from esphome.__main__ import check_permissions, get_port_type + + result = 0 + handled = False + + if get_port_type(host) == "SERIAL": + check_permissions(host) + result = _upload_using_platformio(config, host, ["-t", "upload"]) + handled = True + + if host == "PYOCD": + result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"]) + handled = True + + if result != 0: + raise EsphomeError(f"Upload failed with result: {result}") + + return handled diff --git a/esphome/components/nrf52/boards.py b/esphome/components/nrf52/boards.py new file mode 100644 index 0000000000..8e5fb2a23d --- /dev/null +++ b/esphome/components/nrf52/boards.py @@ -0,0 +1,34 @@ +from esphome.components.zephyr import Section +from esphome.components.zephyr.const import KEY_BOOTLOADER + +from .const import ( + BOOTLOADER_ADAFRUIT, + BOOTLOADER_ADAFRUIT_NRF52_SD132, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, +) + +BOARDS_ZEPHYR = { + "adafruit_itsybitsy_nrf52840": { + KEY_BOOTLOADER: [ + BOOTLOADER_ADAFRUIT, + BOOTLOADER_ADAFRUIT_NRF52_SD132, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, + ] + }, +} + +# https://github.com/ffenix113/zigbee_home/blob/17bb7b9e9d375e756da9e38913f53303937fb66a/types/board/known_boards.go +# https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather?view=all#hathach-memory-map +BOOTLOADER_CONFIG = { + BOOTLOADER_ADAFRUIT_NRF52_SD132: [ + Section("empty_app_offset", 0x0, 0x26000, "flash_primary"), + ], + BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: [ + Section("empty_app_offset", 0x0, 0x26000, "flash_primary"), + ], + BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: [ + Section("empty_app_offset", 0x0, 0x27000, "flash_primary"), + ], +} diff --git a/esphome/components/nrf52/const.py b/esphome/components/nrf52/const.py new file mode 100644 index 0000000000..d827e5fb22 --- /dev/null +++ b/esphome/components/nrf52/const.py @@ -0,0 +1,4 @@ +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" diff --git a/esphome/components/nrf52/gpio.py b/esphome/components/nrf52/gpio.py new file mode 100644 index 0000000000..85230c1f57 --- /dev/null +++ b/esphome/components/nrf52/gpio.py @@ -0,0 +1,53 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components.zephyr.const import zephyr_ns +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INVERTED, CONF_MODE, CONF_NUMBER, PLATFORM_NRF52 + +ZephyrGPIOPin = zephyr_ns.class_("ZephyrGPIOPin", cg.InternalGPIOPin) + + +def _translate_pin(value): + if isinstance(value, dict) or value is None: + raise cv.Invalid( + "This variable only supports pin numbers, not full pin schemas " + "(with inverted and mode)." + ) + if isinstance(value, int): + return value + try: + return int(value) + except ValueError: + pass + # e.g. P0.27 + if len(value) >= len("P0.0") and value[0] == "P" and value[2] == ".": + return cv.int_(value[len("P")].strip()) * 32 + cv.int_( + value[len("P0.") :].strip() + ) + raise cv.Invalid(f"Invalid pin: {value}") + + +def validate_gpio_pin(value): + value = _translate_pin(value) + if value < 0 or value > (32 + 16): + raise cv.Invalid(f"NRF52: Invalid pin number: {value}") + return value + + +NRF52_PIN_SCHEMA = cv.All( + pins.gpio_base_schema( + ZephyrGPIOPin, + validate_gpio_pin, + modes=pins.GPIO_STANDARD_MODES, + ), +) + + +@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_NRF52, NRF52_PIN_SCHEMA) +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])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 4beed57188..90a1619e4c 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -19,6 +19,7 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_VALUE, CONF_WEB_SERVER, + DEVICE_CLASS_ABSOLUTE_HUMIDITY, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, DEVICE_CLASS_AREA, @@ -81,6 +82,7 @@ from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ + DEVICE_CLASS_ABSOLUTE_HUMIDITY, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, DEVICE_CLASS_AREA, diff --git a/esphome/components/one_wire/one_wire.cpp b/esphome/components/one_wire/one_wire.cpp index 131bc4fbfe..96e6145f63 100644 --- a/esphome/components/one_wire/one_wire.cpp +++ b/esphome/components/one_wire/one_wire.cpp @@ -11,8 +11,6 @@ const std::string &OneWireDevice::get_address_name() { return this->address_name_; } -std::string OneWireDevice::unique_id() { return "dallas-" + str_lower_case(format_hex(this->address_)); } - bool OneWireDevice::send_command_(uint8_t cmd) { if (!this->bus_->select(this->address_)) return false; diff --git a/esphome/components/one_wire/one_wire.h b/esphome/components/one_wire/one_wire.h index bf10e4f82e..e83c6e81e8 100644 --- a/esphome/components/one_wire/one_wire.h +++ b/esphome/components/one_wire/one_wire.h @@ -24,8 +24,6 @@ class OneWireDevice { /// Helper to create (and cache) the name for this sensor. For example "0xfe0000031f1eaf29". const std::string &get_address_name(); - std::string unique_id(); - protected: uint64_t address_{0}; OneWireBus *bus_{nullptr}; ///< pointer to OneWireBus instance diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 11ed97831e..0fa299ce5c 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -204,7 +204,7 @@ def add_pio_file(component: str, key: str, data: str): cv.validate_id_name(key) except cv.Invalid as e: raise EsphomeError( - f"[{component}] Invalid PIO key: {key}. Allowed characters: [{ascii_letters}{digits}_]\nPlease report an issue https://github.com/esphome/issues" + f"[{component}] Invalid PIO key: {key}. Allowed characters: [{ascii_letters}{digits}_]\nPlease report an issue https://github.com/esphome/esphome/issues" ) from e CORE.data[KEY_RP2040][KEY_PIO_FILES][key] = data diff --git a/esphome/components/rp2040/helpers.cpp b/esphome/components/rp2040/helpers.cpp index a6eac58dc6..30b40a723a 100644 --- a/esphome/components/rp2040/helpers.cpp +++ b/esphome/components/rp2040/helpers.cpp @@ -44,6 +44,10 @@ void Mutex::unlock() {} IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); } IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } +// RP2040 doesn't support lwIP core locking, so this is a no-op +LwIPLock::LwIPLock() {} +LwIPLock::~LwIPLock() {} + void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #ifdef USE_WIFI WiFi.macAddress(mac); diff --git a/esphome/components/runtime_stats/__init__.py b/esphome/components/runtime_stats/__init__.py new file mode 100644 index 0000000000..a36e8bfd28 --- /dev/null +++ b/esphome/components/runtime_stats/__init__.py @@ -0,0 +1,34 @@ +""" +Runtime statistics component for ESPHome. +""" + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@bdraco"] + +CONF_LOG_INTERVAL = "log_interval" + +runtime_stats_ns = cg.esphome_ns.namespace("runtime_stats") +RuntimeStatsCollector = runtime_stats_ns.class_("RuntimeStatsCollector") + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(RuntimeStatsCollector), + cv.Optional( + CONF_LOG_INTERVAL, default="60s" + ): cv.positive_time_period_milliseconds, + } +) + + +async def to_code(config): + """Generate code for the runtime statistics component.""" + # Define USE_RUNTIME_STATS when this component is used + cg.add_define("USE_RUNTIME_STATS") + + # Create the runtime stats instance (constructor sets global_runtime_stats) + var = cg.new_Pvariable(config[CONF_ID]) + + cg.add(var.set_log_interval(config[CONF_LOG_INTERVAL])) diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp new file mode 100644 index 0000000000..8f5d5daf01 --- /dev/null +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -0,0 +1,102 @@ +#include "runtime_stats.h" + +#ifdef USE_RUNTIME_STATS + +#include "esphome/core/component.h" +#include + +namespace esphome { + +namespace runtime_stats { + +RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0) { + global_runtime_stats = this; +} + +void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) { + 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); + } + + if (this->next_log_time_ == 0) { + this->next_log_time_ = current_time + this->log_interval_; + return; + } +} + +void RuntimeStatsCollector::log_stats_() { + ESP_LOGI(TAG, "Component Runtime Statistics"); + ESP_LOGI(TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_); + + // First collect stats we want to display + std::vector stats_to_display; + + for (const auto &it : this->component_stats_) { + const ComponentRuntimeStats &stats = it.second; + if (stats.get_period_count() > 0) { + ComponentStatPair pair = {it.first, &stats}; + stats_to_display.push_back(pair); + } + } + + // Sort by period runtime (descending) + std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater()); + + // 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()); + } + + // Log total stats since boot + ESP_LOGI(TAG, "Total stats (since boot):"); + + // Re-sort by total runtime for all-time stats + std::sort(stats_to_display.begin(), stats_to_display.end(), + [](const ComponentStatPair &a, const ComponentStatPair &b) { + return a.stats->get_total_time_ms() > b.stats->get_total_time_ms(); + }); + + 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()); + } +} + +void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) { + if (this->next_log_time_ == 0) + return; + + if (current_time >= this->next_log_time_) { + this->log_stats_(); + this->reset_stats_(); + this->next_log_time_ = current_time + this->log_interval_; + } +} + +} // namespace runtime_stats + +runtime_stats::RuntimeStatsCollector *global_runtime_stats = + nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif // USE_RUNTIME_STATS diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h new file mode 100644 index 0000000000..e2f8bee563 --- /dev/null +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -0,0 +1,132 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_RUNTIME_STATS + +#include +#include +#include +#include +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { + +class Component; // Forward declaration + +namespace runtime_stats { + +static const char *const TAG = "runtime_stats"; + +class ComponentRuntimeStats { + public: + ComponentRuntimeStats() + : period_count_(0), + period_time_ms_(0), + period_max_time_ms_(0), + total_count_(0), + total_time_ms_(0), + total_max_time_ms_(0) {} + + void record_time(uint32_t duration_ms) { + // Update period counters + this->period_count_++; + this->period_time_ms_ += duration_ms; + if (duration_ms > this->period_max_time_ms_) + this->period_max_time_ms_ = duration_ms; + + // Update total counters + this->total_count_++; + this->total_time_ms_ += duration_ms; + if (duration_ms > this->total_max_time_ms_) + this->total_max_time_ms_ = duration_ms; + } + + void reset_period_stats() { + this->period_count_ = 0; + this->period_time_ms_ = 0; + this->period_max_time_ms_ = 0; + } + + // Period stats (reset each logging interval) + uint32_t get_period_count() const { return this->period_count_; } + uint32_t get_period_time_ms() const { return this->period_time_ms_; } + uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; } + float get_period_avg_time_ms() const { + return this->period_count_ > 0 ? this->period_time_ms_ / static_cast(this->period_count_) : 0.0f; + } + + // Total stats (persistent until reboot) + uint32_t get_total_count() const { return this->total_count_; } + uint32_t get_total_time_ms() const { return this->total_time_ms_; } + uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; } + float get_total_avg_time_ms() const { + return this->total_count_ > 0 ? this->total_time_ms_ / static_cast(this->total_count_) : 0.0f; + } + + protected: + // Period stats (reset each logging interval) + uint32_t period_count_; + uint32_t period_time_ms_; + uint32_t period_max_time_ms_; + + // Total stats (persistent until reboot) + uint32_t total_count_; + uint32_t total_time_ms_; + uint32_t total_max_time_ms_; +}; + +// For sorting components by run time +struct ComponentStatPair { + const char *name; + const ComponentRuntimeStats *stats; + + bool operator>(const ComponentStatPair &other) const { + // Sort by period time as that's what we're displaying in the logs + return stats->get_period_time_ms() > other.stats->get_period_time_ms(); + } +}; + +class RuntimeStatsCollector { + public: + RuntimeStatsCollector(); + + void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; } + uint32_t get_log_interval() const { return this->log_interval_; } + + void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time); + + // Process any pending stats printing (should be called after component loop) + void process_pending_stats(uint32_t current_time); + + protected: + void log_stats_(); + + void reset_stats_() { + for (auto &it : this->component_stats_) { + it.second.reset_period_stats(); + } + } + + // 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_; + uint32_t log_interval_; + uint32_t next_log_time_; +}; + +} // namespace runtime_stats + +extern runtime_stats::RuntimeStatsCollector + *global_runtime_stats; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif // USE_RUNTIME_STATS diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index f341d2a47b..6981af4de9 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor import esphome.config_validation as cv from esphome.const import ( + CONF_ALTITUDE_COMPENSATION, CONF_AMBIENT_PRESSURE_COMPENSATION, CONF_AUTOMATIC_SELF_CALIBRATION, CONF_CO2, @@ -35,8 +36,6 @@ ForceRecalibrationWithReference = scd30_ns.class_( "ForceRecalibrationWithReference", automation.Action ) -CONF_ALTITUDE_COMPENSATION = "altitude_compensation" - CONFIG_SCHEMA = ( cv.Schema( { diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index fc859d63b8..6b2188cd5a 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -4,6 +4,7 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor import esphome.config_validation as cv from esphome.const import ( + CONF_ALTITUDE_COMPENSATION, CONF_AMBIENT_PRESSURE_COMPENSATION, CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE, CONF_AUTOMATIC_SELF_CALIBRATION, @@ -49,9 +50,6 @@ PerformForcedCalibrationAction = scd4x_ns.class_( ) FactoryResetAction = scd4x_ns.class_("FactoryResetAction", automation.Action) - -CONF_ALTITUDE_COMPENSATION = "altitude_compensation" - CONFIG_SCHEMA = ( cv.Schema( { diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ea74361d51..bcde623df2 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -41,6 +41,7 @@ from esphome.const import ( CONF_VALUE, CONF_WEB_SERVER, CONF_WINDOW_SIZE, + DEVICE_CLASS_ABSOLUTE_HUMIDITY, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, DEVICE_CLASS_AREA, @@ -107,6 +108,7 @@ from esphome.util import Registry CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ + DEVICE_CLASS_ABSOLUTE_HUMIDITY, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, DEVICE_CLASS_AREA, diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index dd8635f0c0..2fd56b7c8f 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -50,6 +50,7 @@ optional MedianFilter::new_value(float value) { 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); diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 7dab63b026..0a82677bc9 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -96,7 +96,6 @@ void Sensor::clear_filters() { } float Sensor::get_state() const { return this->state; } float Sensor::get_raw_state() const { return this->raw_state; } -std::string Sensor::unique_id() { return ""; } void Sensor::internal_send_state_to_frontend(float state) { this->set_has_state(true); diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 3fb6e5522b..c2ded0f2c3 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -28,9 +28,6 @@ namespace sensor { if (!(obj)->get_icon().empty()) { \ ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ } \ - if (!(obj)->unique_id().empty()) { \ - ESP_LOGV(TAG, "%s Unique ID: '%s'", prefix, (obj)->unique_id().c_str()); \ - } \ if ((obj)->get_force_update()) { \ ESP_LOGV(TAG, "%s Force Update: YES", prefix); \ } \ @@ -141,12 +138,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa */ float raw_state; - /** Override this method to set the unique ID of this sensor. - * - * @deprecated Do not use for new sensors, a suitable unique ID is automatically generated (2023.4). - */ - virtual std::string unique_id(); - void internal_send_state_to_frontend(float state); protected: diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 333a076bec..8811ea1644 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -200,7 +200,7 @@ AudioPipelineState AudioPipeline::process_state() { if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) { this->delete_tasks_(); if (this->hard_stop_) { - // Stop command was sent, so immediately end of the playback + // Stop command was sent, so immediately end the playback this->speaker_->stop(); this->hard_stop_ = false; } else { @@ -210,13 +210,25 @@ AudioPipelineState AudioPipeline::process_state() { } } this->is_playing_ = false; - return AudioPipelineState::STOPPED; + if (!this->speaker_->is_running()) { + return AudioPipelineState::STOPPED; + } else { + this->is_finishing_ = true; + } } if (this->pause_state_) { return AudioPipelineState::PAUSED; } + if (this->is_finishing_) { + if (!this->speaker_->is_running()) { + this->is_finishing_ = false; + } else { + return AudioPipelineState::PLAYING; + } + } + if ((this->read_task_handle_ == nullptr) && (this->decode_task_handle_ == nullptr)) { // No tasks are running, so the pipeline is stopped. xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP); diff --git a/esphome/components/speaker/media_player/audio_pipeline.h b/esphome/components/speaker/media_player/audio_pipeline.h index 722d9cbb2a..98f43fda6e 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.h +++ b/esphome/components/speaker/media_player/audio_pipeline.h @@ -114,6 +114,7 @@ class AudioPipeline { bool hard_stop_{false}; bool is_playing_{false}; + bool is_finishing_{false}; bool pause_state_{false}; bool task_stack_in_psram_; diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 58bfc3f411..065ccc2668 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -1,4 +1,5 @@ import re +from typing import Any from esphome import pins import esphome.codegen as cg @@ -139,6 +140,27 @@ def get_hw_interface_list(): return [] +def one_of_interface_validator(additional_values: list[str] | None = None) -> Any: + """Helper to create a one_of validator for SPI interfaces. + + This delays evaluation of get_hw_interface_list() until validation time, + avoiding access to CORE.data during module import. + + Args: + additional_values: List of additional valid values to include + """ + if additional_values is None: + additional_values = [] + + def validator(value: str) -> str: + return cv.one_of( + *sum(get_hw_interface_list(), additional_values), + lower=True, + )(value) + + return cv.All(cv.string, validator) + + # Given an SPI name, return the index of it in the available list def get_spi_index(name): for i, ilist in enumerate(get_hw_interface_list()): @@ -274,9 +296,8 @@ SPI_SINGLE_SCHEMA = cv.All( cv.Optional(CONF_FORCE_SW): cv.invalid( "force_sw is deprecated - use interface: software" ), - cv.Optional(CONF_INTERFACE, default="any"): cv.one_of( - *sum(get_hw_interface_list(), ["software", "hardware", "any"]), - lower=True, + cv.Optional(CONF_INTERFACE, default="any"): one_of_interface_validator( + ["software", "hardware", "any"] ), cv.Optional(CONF_DATA_PINS): cv.invalid( "'data_pins' should be used with 'type: quad or octal' only" @@ -309,10 +330,9 @@ def spi_mode_schema(mode): cv.ensure_list(pins.internal_gpio_output_pin_number), cv.Length(min=pin_count, max=pin_count), ), - cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of( - *sum(get_hw_interface_list(), ["hardware"]), - lower=True, - ), + cv.Optional( + CONF_INTERFACE, default="hardware" + ): one_of_interface_validator(["hardware"]), cv.Optional(CONF_MISO_PIN): cv.invalid( f"'miso_pin' should not be used with {mode} SPI" ), diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 1f039cff78..00425b853f 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -176,7 +176,7 @@ void SSD1306::setup() { // Disable scrolling mode (0x2E) this->command(SSD1306_COMMAND_DEACTIVATE_SCROLL); - // Contrast and brighrness + // Contrast and brightness // SSD1306 does not have brightness setting set_contrast(this->contrast_); if (this->is_ssd1305_()) diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index c57e0ffefb..72b540b84c 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -70,7 +70,5 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->callback_.call(state); } -std::string TextSensor::unique_id() { return ""; } - } // namespace text_sensor } // namespace esphome diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index b27145aa18..b54f75155b 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -20,9 +20,6 @@ namespace text_sensor { if (!(obj)->get_icon().empty()) { \ ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ } \ - if (!(obj)->unique_id().empty()) { \ - ESP_LOGV(TAG, "%s Unique ID: '%s'", prefix, (obj)->unique_id().c_str()); \ - } \ } #define SUB_TEXT_SENSOR(name) \ @@ -64,11 +61,6 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - /** Override this method to set the unique ID of this sensor. - * - * @deprecated Do not use for new sensors, a suitable unique ID is automatically generated (2023.4). - */ - virtual std::string unique_id(); void internal_send_state_to_frontend(const std::string &state); diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index ab821d457b..58d35c4baf 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -6,6 +6,7 @@ import tzlocal from esphome import automation from esphome.automation import Condition 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_AT, @@ -25,7 +26,7 @@ from esphome.const import ( CONF_TIMEZONE, CONF_TRIGGER_ID, ) -from esphome.core import coroutine_with_priority +from esphome.core import CORE, coroutine_with_priority _LOGGER = logging.getLogger(__name__) @@ -341,6 +342,8 @@ async def register_time(time_var, config): @coroutine_with_priority(100.0) async def to_code(config): + if CORE.using_zephyr: + zephyr_add_prj_conf("POSIX_CLOCK", True) cg.add_define("USE_TIME") cg.add_global(time_ns.using) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 61391d2c6b..42c564659f 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -2,13 +2,15 @@ #include "esphome/core/log.h" #ifdef USE_HOST #include +#elif defined(USE_ZEPHYR) +#include #else #include "lwip/opt.h" #endif #ifdef USE_ESP8266 #include "sys/time.h" #endif -#ifdef USE_RP2040 +#if defined(USE_RP2040) || defined(USE_ZEPHYR) #include #endif #include @@ -22,11 +24,22 @@ static const char *const TAG = "time"; RealTimeClock::RealTimeClock() = default; void RealTimeClock::synchronize_epoch_(uint32_t epoch) { + ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); // Update UTC epoch time. +#ifdef USE_ZEPHYR + struct timespec ts; + ts.tv_nsec = 0; + ts.tv_sec = static_cast(epoch); + + int ret = clock_settime(CLOCK_REALTIME, &ts); + + if (ret != 0) { + ESP_LOGW(TAG, "clock_settime() failed with code %d", ret); + } +#else struct timeval timev { .tv_sec = static_cast(epoch), .tv_usec = 0, }; - ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); struct timezone tz = {0, 0}; int ret = settimeofday(&timev, &tz); if (ret == EINVAL) { @@ -43,7 +56,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { if (ret != 0) { ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); } - +#endif auto time = this->now(); 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); diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp index fa6b9d621d..54260d7e80 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp @@ -27,7 +27,6 @@ void UptimeSecondsSensor::update() { const float seconds = float(seconds_int) + (this->uptime_ % 1000ULL) / 1000.0f; this->publish_state(seconds); } -std::string UptimeSecondsSensor::unique_id() { return get_mac_address() + "-uptime"; } float UptimeSecondsSensor::get_setup_priority() const { return setup_priority::HARDWARE; } void UptimeSecondsSensor::dump_config() { LOG_SENSOR("", "Uptime Sensor", this); diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.h b/esphome/components/uptime/sensor/uptime_seconds_sensor.h index 41b3647822..210195052f 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.h +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.h @@ -13,8 +13,6 @@ class UptimeSecondsSensor : public sensor::Sensor, public PollingComponent { float get_setup_priority() const override; - std::string unique_id() override; - protected: uint64_t uptime_{0}; }; diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 5b2437ab62..ed093595cc 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -17,7 +17,6 @@ void VersionTextSensor::setup() { } float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; } void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } -std::string VersionTextSensor::unique_id() { return get_mac_address() + "-version"; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } } // namespace version diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index 9355e78442..6813da7830 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -12,7 +12,6 @@ class VersionTextSensor : public text_sensor::TextSensor, public Component { void setup() override; void dump_config() override; float get_setup_priority() const override; - std::string unique_id() override; protected: bool hide_timestamp_{false}; diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 9cf7d10936..3c69dafa43 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -35,6 +35,27 @@ void VoiceAssistant::setup() { temp_ring_buffer->write((void *) data.data(), data.size()); } }); + +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + this->media_player_->add_on_state_callback([this]() { + switch (this->media_player_->state) { + case media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING: + if (this->media_player_response_state_ == MediaPlayerResponseState::URL_SENT) { + // State changed to announcing after receiving the url + this->media_player_response_state_ = MediaPlayerResponseState::PLAYING; + } + break; + default: + if (this->media_player_response_state_ == MediaPlayerResponseState::PLAYING) { + // No longer announcing the TTS response + this->media_player_response_state_ = MediaPlayerResponseState::FINISHED; + } + break; + } + }); + } +#endif } float VoiceAssistant::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } @@ -223,7 +244,15 @@ void VoiceAssistant::loop() { msg.wake_word_phrase = this->wake_word_; this->wake_word_ = ""; - if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) { + // Reset media player state tracking +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + this->media_player_response_state_ = MediaPlayerResponseState::IDLE; + } +#endif + + if (this->api_client_ == nullptr || + !this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE)) { ESP_LOGW(TAG, "Could not request start"); this->error_trigger_->trigger("not-connected", "Could not request start"); this->continuous_ = false; @@ -245,7 +274,7 @@ void VoiceAssistant::loop() { if (this->audio_mode_ == AUDIO_MODE_API) { api::VoiceAssistantAudio msg; msg.data.assign((char *) this->send_buffer_, read_bytes); - this->api_client_->send_message(msg); + this->api_client_->send_message(msg, api::VoiceAssistantAudio::MESSAGE_TYPE); } else { if (!this->udp_socket_running_) { if (!this->start_udp_socket_()) { @@ -314,24 +343,17 @@ void VoiceAssistant::loop() { #endif #ifdef USE_MEDIA_PLAYER if (this->media_player_ != nullptr) { - playing = (this->media_player_->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING); + playing = (this->media_player_response_state_ == MediaPlayerResponseState::PLAYING); - if (playing && this->media_player_wait_for_announcement_start_) { - // Announcement has started playing, wait for it to finish - this->media_player_wait_for_announcement_start_ = false; - this->media_player_wait_for_announcement_end_ = true; - } - - if (!playing && this->media_player_wait_for_announcement_end_) { - // Announcement has finished playing - this->media_player_wait_for_announcement_end_ = false; + if (this->media_player_response_state_ == MediaPlayerResponseState::FINISHED) { + this->media_player_response_state_ = MediaPlayerResponseState::IDLE; this->cancel_timeout("playing"); ESP_LOGD(TAG, "Announcement finished playing"); this->set_state_(State::RESPONSE_FINISHED, State::RESPONSE_FINISHED); api::VoiceAssistantAnnounceFinished msg; msg.success = true; - this->api_client_->send_message(msg); + this->api_client_->send_message(msg, api::VoiceAssistantAnnounceFinished::MESSAGE_TYPE); break; } } @@ -555,7 +577,7 @@ void VoiceAssistant::request_stop() { break; case State::AWAITING_RESPONSE: this->signal_stop_(); - // Fallthrough intended to stop a streaming TTS announcement that has potentially started + break; case State::STREAMING_RESPONSE: #ifdef USE_MEDIA_PLAYER // Stop any ongoing media player announcement @@ -565,6 +587,10 @@ void VoiceAssistant::request_stop() { .set_announcement(true) .perform(); } + if (this->started_streaming_tts_) { + // Haven't reached the TTS_END stage, so send the stop signal to HA. + this->signal_stop_(); + } #endif break; case State::RESPONSE_FINISHED: @@ -580,7 +606,7 @@ void VoiceAssistant::signal_stop_() { ESP_LOGD(TAG, "Signaling stop"); api::VoiceAssistantRequest msg; msg.start = false; - this->api_client_->send_message(msg); + this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE); } void VoiceAssistant::start_playback_timeout_() { @@ -590,7 +616,7 @@ void VoiceAssistant::start_playback_timeout_() { api::VoiceAssistantAnnounceFinished msg; msg.success = true; - this->api_client_->send_message(msg); + this->api_client_->send_message(msg, api::VoiceAssistantAnnounceFinished::MESSAGE_TYPE); }); } @@ -648,13 +674,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { if (this->media_player_ != nullptr) { for (const auto &arg : msg.data) { if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) { + this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT; + this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform(); - this->media_player_wait_for_announcement_start_ = true; - this->media_player_wait_for_announcement_end_ = false; this->started_streaming_tts_ = true; + this->start_playback_timeout_(); + tts_url_for_trigger = this->tts_response_url_; this->tts_response_url_.clear(); // Reset streaming URL + this->set_state_(State::STREAMING_RESPONSE, State::STREAMING_RESPONSE); } } } @@ -713,18 +742,22 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->defer([this, url]() { #ifdef USE_MEDIA_PLAYER if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) { + this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT; + this->media_player_->make_call().set_media_url(url).set_announcement(true).perform(); - this->media_player_wait_for_announcement_start_ = true; - this->media_player_wait_for_announcement_end_ = false; - // Start the playback timeout, as the media player state isn't immediately updated this->start_playback_timeout_(); } + this->started_streaming_tts_ = false; // Helps indicate reaching the TTS_END stage #endif this->tts_end_trigger_->trigger(url); }); State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE; - this->set_state_(new_state, new_state); + if (new_state != this->state_) { + // Don't needlessly change the state. The intent progress stage may have already changed the state to streaming + // response. + this->set_state_(new_state, new_state); + } break; } case api::enums::VOICE_ASSISTANT_RUN_END: { @@ -875,6 +908,9 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) #ifdef USE_MEDIA_PLAYER if (this->media_player_ != nullptr) { this->tts_start_trigger_->trigger(msg.text); + + this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT; + if (!msg.preannounce_media_id.empty()) { this->media_player_->make_call().set_media_url(msg.preannounce_media_id).set_announcement(true).perform(); } @@ -886,9 +922,6 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) .perform(); this->continue_conversation_ = msg.start_conversation; - this->media_player_wait_for_announcement_start_ = true; - this->media_player_wait_for_announcement_end_ = false; - // Start the playback timeout, as the media player state isn't immediately updated this->start_playback_timeout_(); if (this->continuous_) { diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 2424ea6052..95f77dbf09 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -90,6 +90,15 @@ struct Configuration { uint32_t max_active_wake_words; }; +#ifdef USE_MEDIA_PLAYER +enum class MediaPlayerResponseState { + IDLE, + URL_SENT, + PLAYING, + FINISHED, +}; +#endif + class VoiceAssistant : public Component { public: VoiceAssistant(); @@ -272,8 +281,8 @@ class VoiceAssistant : public Component { media_player::MediaPlayer *media_player_{nullptr}; std::string tts_response_url_{""}; bool started_streaming_tts_{false}; - bool media_player_wait_for_announcement_start_{false}; - bool media_player_wait_for_announcement_end_{false}; + + MediaPlayerResponseState media_player_response_state_{MediaPlayerResponseState::IDLE}; #endif bool local_output_{false}; diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 6890f60014..8ead14dcac 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -74,13 +74,14 @@ def validate_local(config: ConfigType) -> ConfigType: return config -def validate_ota_removed(config: ConfigType) -> ConfigType: - # Only raise error if OTA is explicitly enabled (True) - # If it's False or not specified, we can safely ignore it - if config.get(CONF_OTA): +def validate_ota(config: ConfigType) -> ConfigType: + # The OTA option only accepts False to explicitly disable OTA for web_server + # IMPORTANT: Setting ota: false ONLY affects the web_server component + # The captive_portal component will still be able to perform OTA updates + if CONF_OTA in config and config[CONF_OTA] is not False: raise cv.Invalid( - f"The '{CONF_OTA}' option has been removed from 'web_server'. " - f"Please use the new OTA platform structure instead:\n\n" + f"The '{CONF_OTA}' option in 'web_server' only accepts 'false' to disable OTA. " + f"To enable OTA, please use the new OTA platform structure instead:\n\n" f"ota:\n" f" - platform: web_server\n\n" f"See https://esphome.io/components/ota for more information." @@ -185,7 +186,7 @@ CONFIG_SCHEMA = cv.All( web_server_base.WebServerBase ), cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, - cv.Optional(CONF_OTA, default=False): cv.boolean, + cv.Optional(CONF_OTA): cv.boolean, cv.Optional(CONF_LOG, default=True): cv.boolean, cv.Optional(CONF_LOCAL): cv.boolean, cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group), @@ -203,7 +204,7 @@ CONFIG_SCHEMA = cv.All( default_url, validate_local, validate_sorting_groups, - validate_ota_removed, + validate_ota, ) @@ -288,7 +289,11 @@ async def to_code(config): cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) # OTA is now handled by the web_server OTA platform - # The CONF_OTA option is kept only for backwards compatibility validation + # The CONF_OTA option is kept to allow explicitly disabling OTA for web_server + # IMPORTANT: This ONLY affects the web_server component, NOT captive_portal + # Captive portal will still be able to perform OTA updates even when this is set + if config.get(CONF_OTA) is False: + cg.add_define("USE_WEBSERVER_OTA_DISABLED") cg.add(var.set_expose_log(config[CONF_LOG])) if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS") @@ -312,3 +317,15 @@ async def to_code(config): if (sorting_group_config := config.get(CONF_SORTING_GROUPS)) is not None: cg.add_define("USE_WEBSERVER_SORTING") add_sorting_groups(var, sorting_group_config) + + +def FILTER_SOURCE_FILES() -> list[str]: + """Filter out web_server_v1.cpp when version is not 1.""" + files_to_filter: list[str] = [] + + # web_server_v1.cpp is only needed when version is 1 + config = CORE.config.get("web_server", {}) + if config.get(CONF_VERSION, 2) != 1: + files_to_filter.append("web_server_v1.cpp") + + return files_to_filter diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 4f8f6fda17..966c1c1024 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -5,6 +5,10 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" +#ifdef USE_CAPTIVE_PORTAL +#include "esphome/components/captive_portal/captive_portal.h" +#endif + #ifdef USE_ARDUINO #ifdef USE_ESP8266 #include @@ -25,7 +29,22 @@ class OTARequestHandler : public AsyncWebHandler { void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) override; bool canHandle(AsyncWebServerRequest *request) const override { - return request->url() == "/update" && request->method() == HTTP_POST; + // Check if this is an OTA update request + bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST; + +#if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL) + // IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component + // Captive portal can still perform OTA updates - check if request is from active captive portal + // Note: global_captive_portal is the standard way components communicate in ESPHome + return is_ota_request && captive_portal::global_captive_portal != nullptr && + captive_portal::global_captive_portal->is_active(); +#elif defined(USE_WEBSERVER_OTA_DISABLED) + // OTA disabled for web_server and no captive portal compiled in + return false; +#else + // OTA enabled for web_server + return is_ota_request; +#endif } // NOLINTNEXTLINE(readability-identifier-naming) @@ -152,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin // Finalize if (final) { - ESP_LOGD(TAG, "OTA final chunk: index=%u, len=%u, total_read=%u, contentLength=%u", index, len, + ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%u, contentLength=%zu", index, len, this->ota_read_length_, request->contentLength()); // For Arduino framework, the Update library tracks expected size from firmware header diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 9ec667dbc5..deddea5250 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -268,10 +268,10 @@ 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(); -#ifdef USE_WEBSERVER_OTA - root["ota"] = true; // web_server OTA platform is configured +#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 #else - root["ota"] = false; + root["ota"] = true; #endif root["log"] = this->expose_log_; root["lang"] = "en"; @@ -1620,7 +1620,9 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } -static std::string get_event_type(event::Event *event) { return event->last_event_type ? *event->last_event_type : ""; } +static std::string get_event_type(event::Event *event) { + return (event && event->last_event_type) ? *event->last_event_type : ""; +} std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { auto *event = static_cast(source); diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index 5db0f1cae9..0f558f6d81 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -192,7 +192,9 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream->print(F("

See ESPHome Web API for " "REST API documentation.

")); -#ifdef USE_WEBSERVER_OTA +#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

")); #endif diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index a7877eb90b..b3167c5696 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -20,10 +20,6 @@ #include "lwip/dns.h" #include "lwip/err.h" -#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING -#include "lwip/priv/tcpip_priv.h" -#endif - #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -295,25 +291,16 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { } 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 -#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING - if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { - LOCK_TCPIP_CORE(); + // 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); } -#endif - - // 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); - -#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING - if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { - UNLOCK_TCPIP_CORE(); - } -#endif // No manual IP is set; use DHCP client if (dhcp_status != ESP_NETIF_DHCP_STARTED) { diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 0aa44a0894..68b5f438e4 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -28,7 +28,6 @@ class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSenso } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } - std::string unique_id() override { return get_mac_address() + "-wifiinfo-ip"; } void dump_config() override; void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } @@ -51,7 +50,6 @@ class DNSAddressWifiInfo : public PollingComponent, public text_sensor::TextSens } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } - std::string unique_id() override { return get_mac_address() + "-wifiinfo-dns"; } void dump_config() override; protected: @@ -80,7 +78,6 @@ class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSen } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } - std::string unique_id() override { return get_mac_address() + "-wifiinfo-scanresults"; } void dump_config() override; protected: @@ -97,7 +94,6 @@ class SSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } - std::string unique_id() override { return get_mac_address() + "-wifiinfo-ssid"; } void dump_config() override; protected: @@ -116,7 +112,6 @@ class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } - std::string unique_id() override { return get_mac_address() + "-wifiinfo-bssid"; } void dump_config() override; protected: @@ -126,7 +121,6 @@ class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { public: void setup() override { this->publish_state(get_mac_address_pretty()); } - std::string unique_id() override { return get_mac_address() + "-wifiinfo-macadr"; } void dump_config() override; }; diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.h b/esphome/components/wifi_signal/wifi_signal_sensor.h index fbe03a6404..5cfd19b523 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.h +++ b/esphome/components/wifi_signal/wifi_signal_sensor.h @@ -13,7 +13,6 @@ class WiFiSignalSensor : public sensor::Sensor, public PollingComponent { void update() override { this->publish_state(wifi::global_wifi_component->wifi_rssi()); } void dump_config() override; - std::string unique_id() override { return get_mac_address() + "-wifisignal"; } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } }; diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp index 1f61e2dda3..4efcf13e08 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -8,6 +8,7 @@ #include "esphome/core/log.h" #include "esphome/core/time.h" #include "esphome/components/network/util.h" +#include "esphome/core/helpers.h" #include #include @@ -42,7 +43,10 @@ void Wireguard::setup() { this->publish_enabled_state(); - this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_)); + { + LwIPLock lock; + this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_)); + } if (this->wg_initialized_ == ESP_OK) { ESP_LOGI(TAG, "Initialized"); @@ -249,7 +253,10 @@ void Wireguard::start_connection_() { } ESP_LOGD(TAG, "Starting connection"); - this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_)); + { + LwIPLock lock; + this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_)); + } if (this->wg_connected_ == ESP_OK) { ESP_LOGI(TAG, "Connection started"); @@ -280,7 +287,10 @@ void Wireguard::start_connection_() { void Wireguard::stop_connection_() { if (this->wg_initialized_ == ESP_OK && this->wg_connected_ == ESP_OK) { ESP_LOGD(TAG, "Stopping connection"); - esp_wireguard_disconnect(&(this->wg_ctx_)); + { + LwIPLock lock; + esp_wireguard_disconnect(&(this->wg_ctx_)); + } this->wg_connected_ = ESP_FAIL; } } diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py new file mode 100644 index 0000000000..2b542404a5 --- /dev/null +++ b/esphome/components/zephyr/__init__.py @@ -0,0 +1,231 @@ +import os +from typing import Final, TypedDict + +import esphome.codegen as cg +from esphome.const import CONF_BOARD +from esphome.core import CORE +from esphome.helpers import copy_file_if_changed, write_file_if_changed + +from .const import ( + BOOTLOADER_MCUBOOT, + KEY_BOOTLOADER, + KEY_EXTRA_BUILD_FILES, + KEY_OVERLAY, + KEY_PM_STATIC, + KEY_PRJ_CONF, + KEY_ZEPHYR, + zephyr_ns, +) + +CODEOWNERS = ["@tomaszduda23"] +AUTO_LOAD = ["preferences"] +KEY_BOARD: Final = "board" + +PrjConfValueType = bool | str | int + + +class Section: + def __init__(self, name, address, size, region): + self.name = name + self.address = address + self.size = size + self.region = region + self.end_address = self.address + self.size + + def __str__(self): + return ( + f"{self.name}:\n" + f" address: 0x{self.address:X}\n" + f" end_address: 0x{self.end_address:X}\n" + f" region: {self.region}\n" + f" size: 0x{self.size:X}" + ) + + +class ZephyrData(TypedDict): + board: str + bootloader: str + prj_conf: dict[str, tuple[PrjConfValueType, bool]] + overlay: str + extra_build_files: dict[str, str] + pm_static: list[Section] + + +def zephyr_set_core_data(config): + CORE.data[KEY_ZEPHYR] = ZephyrData( + board=config[CONF_BOARD], + bootloader=config[KEY_BOOTLOADER], + prj_conf={}, + overlay="", + extra_build_files={}, + pm_static=[], + ) + return config + + +def zephyr_data() -> ZephyrData: + return CORE.data[KEY_ZEPHYR] + + +def zephyr_add_prj_conf( + name: str, value: PrjConfValueType, required: bool = True +) -> None: + """Set an zephyr prj conf value.""" + if not name.startswith("CONFIG_"): + name = "CONFIG_" + name + prj_conf = zephyr_data()[KEY_PRJ_CONF] + if name not in prj_conf: + prj_conf[name] = (value, required) + return + old_value, old_required = prj_conf[name] + if old_value != value and old_required: + raise ValueError( + f"{name} already set with value '{old_value}', cannot set again to '{value}'" + ) + if required: + prj_conf[name] = (value, required) + + +def zephyr_add_overlay(content): + zephyr_data()[KEY_OVERLAY] += content + + +def add_extra_build_file(filename: str, path: str) -> 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: + extra_build_files[filename] = path + return True + return False + + +def add_extra_script(stage: str, filename: str, path: str): + """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 zephyr_to_code(config): + cg.add(zephyr_ns.setup_preferences()) + cg.add_build_flag("-DUSE_ZEPHYR") + cg.set_cpp_standard("gnu++20") + # build is done by west so bypass board checking in platformio + cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards")) + + # c++ support + zephyr_add_prj_conf("NEWLIB_LIBC", True) + zephyr_add_prj_conf("CONFIG_FPU", True) + zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True) + zephyr_add_prj_conf("CPLUSPLUS", True) + zephyr_add_prj_conf("CONFIG_STD_CPP20", True) + zephyr_add_prj_conf("LIB_CPLUSPLUS", True) + # preferences + zephyr_add_prj_conf("SETTINGS", True) + zephyr_add_prj_conf("NVS", True) + zephyr_add_prj_conf("FLASH_MAP", True) + zephyr_add_prj_conf("CONFIG_FLASH", True) + # watchdog + zephyr_add_prj_conf("WATCHDOG", True) + zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False) + # disable console + zephyr_add_prj_conf("UART_CONSOLE", False) + zephyr_add_prj_conf("CONSOLE", False, False) + # use NFC pins as GPIO + zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) + + # os: ***** USAGE FAULT ***** + # os: Illegal load of EXC_RETURN into PC + zephyr_add_prj_conf("MAIN_STACK_SIZE", 2048) + + add_extra_script( + "pre", + "pre_build.py", + os.path.join(os.path.dirname(__file__), "pre_build.py.script"), + ) + + +def _format_prj_conf_val(value: PrjConfValueType) -> str: + if isinstance(value, bool): + return "y" if value else "n" + if isinstance(value, int): + return str(value) + if isinstance(value, str): + return f'"{value}"' + raise ValueError + + +def zephyr_add_cdc_acm(config, id): + zephyr_add_prj_conf("USB_DEVICE_STACK", True) + zephyr_add_prj_conf("USB_CDC_ACM", True) + # prevent device to go to susspend, without this communication stop working in python + # there should be a way to solve it + zephyr_add_prj_conf("USB_DEVICE_REMOTE_WAKEUP", False) + # prevent logging when buffer is full + zephyr_add_prj_conf("USB_CDC_ACM_LOG_LEVEL_WRN", True) + zephyr_add_overlay( + f""" +&zephyr_udc0 {{ + cdc_acm_uart{id}: cdc_acm_uart{id} {{ + compatible = "zephyr,cdc-acm-uart"; + }}; +}}; +""" + ) + + +def zephyr_add_pm_static(section: Section): + CORE.data[KEY_ZEPHYR][KEY_PM_STATIC].extend(section) + + +def copy_files(): + want_opts = zephyr_data()[KEY_PRJ_CONF] + + prj_conf = ( + "\n".join( + f"{name}={_format_prj_conf_val(value[0])}" + for name, value in sorted(want_opts.items()) + ) + + "\n" + ) + + write_file_if_changed(CORE.relative_build_path("zephyr/prj.conf"), prj_conf) + + write_file_if_changed( + CORE.relative_build_path("zephyr/app.overlay"), + zephyr_data()[KEY_OVERLAY], + ) + + if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT or zephyr_data()[ + KEY_BOARD + ] 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" +} +""" + write_file_if_changed( + CORE.relative_build_path(f"boards/{zephyr_data()[KEY_BOARD]}.json"), + fake_board_manifest, + ) + + for filename, path in zephyr_data()[KEY_EXTRA_BUILD_FILES].items(): + copy_file_if_changed( + path, + CORE.relative_build_path(filename), + ) + + pm_static = "\n".join(str(item) for item in zephyr_data()[KEY_PM_STATIC]) + if pm_static: + write_file_if_changed( + CORE.relative_build_path("zephyr/pm_static.yml"), pm_static + ) diff --git a/esphome/components/zephyr/const.py b/esphome/components/zephyr/const.py new file mode 100644 index 0000000000..f14a326344 --- /dev/null +++ b/esphome/components/zephyr/const.py @@ -0,0 +1,14 @@ +from typing import Final + +import esphome.codegen as cg + +BOOTLOADER_MCUBOOT = "mcuboot" + +KEY_BOOTLOADER: Final = "bootloader" +KEY_EXTRA_BUILD_FILES: Final = "extra_build_files" +KEY_OVERLAY: Final = "overlay" +KEY_PM_STATIC: Final = "pm_static" +KEY_PRJ_CONF: Final = "prj_conf" +KEY_ZEPHYR = "zephyr" + +zephyr_ns = cg.esphome_ns.namespace("zephyr") diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp new file mode 100644 index 0000000000..ad7a148cdb --- /dev/null +++ b/esphome/components/zephyr/core.cpp @@ -0,0 +1,90 @@ +#ifdef USE_ZEPHYR + +#include +#include +#include +#include +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" + +namespace esphome { + +static int wdt_channel_id = -1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); + +void yield() { ::k_yield(); } +uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } +uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); } +void delayMicroseconds(uint32_t us) { ::k_usleep(us); } +void delay(uint32_t ms) { ::k_msleep(ms); } + +void arch_init() { + if (device_is_ready(WDT)) { + static wdt_timeout_cfg wdt_config{}; + wdt_config.flags = WDT_FLAG_RESET_SOC; + 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); + } + } +} + +void arch_feed_wdt() { + if (wdt_channel_id >= 0) { + wdt_feed(WDT, wdt_channel_id); + } +} + +void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } +uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); } +uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); } +uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } + +Mutex::Mutex() { + auto *mutex = new k_mutex(); + this->handle_ = mutex; + k_mutex_init(mutex); +} +Mutex::~Mutex() { delete static_cast(this->handle_); } +void Mutex::lock() { k_mutex_lock(static_cast(this->handle_), K_FOREVER); } +bool Mutex::try_lock() { return k_mutex_lock(static_cast(this->handle_), K_NO_WAIT) == 0; } +void Mutex::unlock() { k_mutex_unlock(static_cast(this->handle_)); } + +IRAM_ATTR InterruptLock::InterruptLock() { state_ = irq_lock(); } +IRAM_ATTR InterruptLock::~InterruptLock() { irq_unlock(state_); } + +// Zephyr doesn't support lwIP core locking, so this is a no-op +LwIPLock::LwIPLock() {} +LwIPLock::~LwIPLock() {} + +uint32_t random_uint32() { return rand(); } // NOLINT(cert-msc30-c, cert-msc50-cpp) +bool random_bytes(uint8_t *data, size_t len) { + sys_rand_get(data, len); + return true; +} + +void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) + mac[0] = ((NRF_FICR->DEVICEADDR[1] & 0xFFFF) >> 8) | 0xC0; + mac[1] = NRF_FICR->DEVICEADDR[1] & 0xFFFF; + mac[2] = NRF_FICR->DEVICEADDR[0] >> 24; + mac[3] = NRF_FICR->DEVICEADDR[0] >> 16; + mac[4] = NRF_FICR->DEVICEADDR[0] >> 8; + mac[5] = NRF_FICR->DEVICEADDR[0]; +} + +} // namespace esphome + +void setup(); +void loop(); + +int main() { + setup(); + while (true) { + loop(); + esphome::yield(); + } + return 0; +} + +#endif diff --git a/esphome/components/zephyr/gpio.cpp b/esphome/components/zephyr/gpio.cpp new file mode 100644 index 0000000000..4b84910368 --- /dev/null +++ b/esphome/components/zephyr/gpio.cpp @@ -0,0 +1,120 @@ +#ifdef USE_ZEPHYR +#include "gpio.h" +#include +#include "esphome/core/log.h" + +namespace esphome { +namespace zephyr { + +static const char *const TAG = "zephyr"; + +static int flags_to_mode(gpio::Flags flags, bool inverted, bool value) { + int ret = 0; + if (flags & gpio::FLAG_INPUT) { + ret |= GPIO_INPUT; + } + if (flags & gpio::FLAG_OUTPUT) { + ret |= GPIO_OUTPUT; + if (value != inverted) { + ret |= GPIO_OUTPUT_INIT_HIGH; + } else { + ret |= GPIO_OUTPUT_INIT_LOW; + } + } + if (flags & gpio::FLAG_PULLUP) { + ret |= GPIO_PULL_UP; + } + if (flags & gpio::FLAG_PULLDOWN) { + ret |= GPIO_PULL_DOWN; + } + if (flags & gpio::FLAG_OPEN_DRAIN) { + ret |= GPIO_OPEN_DRAIN; + } + return ret; +} + +struct ISRPinArg { + uint8_t pin; + bool inverted; +}; + +ISRInternalGPIOPin ZephyrGPIOPin::to_isr() const { + auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) + arg->pin = this->pin_; + arg->inverted = this->inverted_; + return ISRInternalGPIOPin((void *) arg); +} + +void ZephyrGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { + // TODO +} + +void ZephyrGPIOPin::setup() { + const struct device *gpio = nullptr; + if (this->pin_ < 32) { +#define GPIO0 DT_NODELABEL(gpio0) +#if DT_NODE_HAS_STATUS(GPIO0, okay) + gpio = DEVICE_DT_GET(GPIO0); +#else +#error "gpio0 is disabled" +#endif + } else { +#define GPIO1 DT_NODELABEL(gpio1) +#if DT_NODE_HAS_STATUS(GPIO1, okay) + gpio = DEVICE_DT_GET(GPIO1); +#else +#error "gpio1 is disabled" +#endif + } + if (device_is_ready(gpio)) { + this->gpio_ = gpio; + } else { + ESP_LOGE(TAG, "gpio %u is not ready.", this->pin_); + return; + } + this->pin_mode(this->flags_); +} + +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_)); +} + +std::string ZephyrGPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "GPIO%u, P%u.%u", this->pin_, this->pin_ / 32, this->pin_ % 32); + return buffer; +} + +bool ZephyrGPIOPin::digital_read() { + if (nullptr == this->gpio_) { + return false; + } + return bool(gpio_pin_get(this->gpio_, this->pin_ % 32) != this->inverted_); +} + +void ZephyrGPIOPin::digital_write(bool value) { + // make sure that value is not ignored since it can be inverted e.g. on switch side + // that way init state should be correct + this->value_ = value; + if (nullptr == this->gpio_) { + return; + } + gpio_pin_set(this->gpio_, this->pin_ % 32, value != this->inverted_ ? 1 : 0); +} +void ZephyrGPIOPin::detach_interrupt() const { + // TODO +} + +} // namespace zephyr + +bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { + // TODO + return false; +} + +} // namespace esphome + +#endif diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h new file mode 100644 index 0000000000..f512ae4648 --- /dev/null +++ b/esphome/components/zephyr/gpio.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef USE_ZEPHYR +#include "esphome/core/hal.h" +struct device; +namespace esphome { +namespace zephyr { + +class ZephyrGPIOPin : public InternalGPIOPin { + public: + void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_inverted(bool inverted) { this->inverted_ = inverted; } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } + + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + void detach_interrupt() const override; + ISRInternalGPIOPin to_isr() const override; + uint8_t get_pin() const override { return this->pin_; } + bool is_inverted() const override { return this->inverted_; } + gpio::Flags get_flags() const override { return flags_; } + + 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; +}; + +} // namespace zephyr +} // namespace esphome + +#endif // USE_ZEPHYR diff --git a/esphome/components/zephyr/pre_build.py.script b/esphome/components/zephyr/pre_build.py.script new file mode 100644 index 0000000000..3731fccf53 --- /dev/null +++ b/esphome/components/zephyr/pre_build.py.script @@ -0,0 +1,4 @@ +Import("env") + +board_config = env.BoardConfig() +board_config.update("frameworks", ["arduino", "zephyr"]) diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp new file mode 100644 index 0000000000..d702366044 --- /dev/null +++ b/esphome/components/zephyr/preferences.cpp @@ -0,0 +1,156 @@ +#ifdef USE_ZEPHYR + +#include +#include "esphome/core/preferences.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace zephyr { + +static const char *const TAG = "zephyr.preferences"; + +#define ESPHOME_SETTINGS_KEY "esphome" + +class ZephyrPreferenceBackend : public ESPPreferenceBackend { + public: + ZephyrPreferenceBackend(uint32_t type) { this->type_ = type; } + ZephyrPreferenceBackend(uint32_t type, std::vector &&data) : data(std::move(data)) { this->type_ = type; } + + bool save(const uint8_t *data, size_t len) override { + this->data.resize(len); + std::memcpy(this->data.data(), data, len); + ESP_LOGVV(TAG, "save key: %u, len: %d", this->type_, len); + return true; + } + + bool load(uint8_t *data, size_t len) override { + if (len != this->data.size()) { + ESP_LOGE(TAG, "size of setting key %s changed, from: %u, to: %u", get_key().c_str(), this->data.size(), len); + return false; + } + std::memcpy(data, this->data.data(), len); + ESP_LOGVV(TAG, "load key: %u, len: %d", this->type_, len); + return true; + } + + uint32_t get_type() const { return this->type_; } + std::string get_key() const { return str_sprintf(ESPHOME_SETTINGS_KEY "/%" PRIx32, this->type_); } + + std::vector data; + + protected: + uint32_t type_ = 0; +}; + +class ZephyrPreferences : public ESPPreferences { + public: + void open() { + int err = settings_subsys_init(); + if (err) { + ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err); + return; + } + + static struct settings_handler settings_cb = { + .name = ESPHOME_SETTINGS_KEY, + .h_set = load_setting, + .h_export = export_settings, + }; + + err = settings_register(&settings_cb); + if (err) { + ESP_LOGE(TAG, "setting_register failed, err, %d", err); + return; + } + + err = settings_load_subtree(ESPHOME_SETTINGS_KEY); + if (err) { + ESP_LOGE(TAG, "Cannot load settings, err: %d", err); + return; + } + ESP_LOGD(TAG, "Loaded %u settings.", this->backends_.size()); + } + + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { + return make_preference(length, type); + } + + ESPPreferenceObject make_preference(size_t length, uint32_t type) override { + for (auto *backend : this->backends_) { + if (backend->get_type() == type) { + return ESPPreferenceObject(backend); + } + } + printf("type %u size %u\n", type, this->backends_.size()); + auto *pref = new ZephyrPreferenceBackend(type); // NOLINT(cppcoreguidelines-owning-memory) + ESP_LOGD(TAG, "Add new setting %s.", pref->get_key().c_str()); + this->backends_.push_back(pref); + return ESPPreferenceObject(pref); + } + + bool sync() override { + ESP_LOGD(TAG, "Save settings"); + int err = settings_save(); + if (err) { + ESP_LOGE(TAG, "Cannot save settings, err: %d", err); + return false; + } + return true; + } + + bool reset() override { + ESP_LOGD(TAG, "Reset settings"); + for (auto *backend : this->backends_) { + // save empty delete data + backend->data.clear(); + } + sync(); + return true; + } + + protected: + std::vector backends_; + + static int load_setting(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) { + auto type = parse_hex(name); + if (!type.has_value()) { + std::string full_name(ESPHOME_SETTINGS_KEY); + full_name += "/"; + full_name += name; + // Delete unusable keys. Otherwise it will stay in flash forever. + settings_delete(full_name.c_str()); + return 1; + } + std::vector data(len); + int err = read_cb(cb_arg, data.data(), len); + + ESP_LOGD(TAG, "load setting, name: %s(%u), len %u, err %u", name, *type, len, err); + auto *pref = new ZephyrPreferenceBackend(*type, std::move(data)); // NOLINT(cppcoreguidelines-owning-memory) + static_cast(global_preferences)->backends_.push_back(pref); + return 0; + } + + static int export_settings(int (*cb)(const char *name, const void *value, size_t val_len)) { + for (auto *backend : static_cast(global_preferences)->backends_) { + auto name = backend->get_key(); + int err = cb(name.c_str(), backend->data.data(), backend->data.size()); + ESP_LOGD(TAG, "save in flash, name %s, len %u, err %d", name.c_str(), backend->data.size(), err); + } + return 0; + } +}; + +void setup_preferences() { + auto *prefs = new ZephyrPreferences(); // NOLINT(cppcoreguidelines-owning-memory) + global_preferences = prefs; + prefs->open(); +} + +} // namespace zephyr + +ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome + +#endif diff --git a/esphome/components/zephyr/preferences.h b/esphome/components/zephyr/preferences.h new file mode 100644 index 0000000000..6a37e41b46 --- /dev/null +++ b/esphome/components/zephyr/preferences.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef USE_ZEPHYR + +namespace esphome { +namespace zephyr { + +void setup_preferences(); + +} // namespace zephyr +} // namespace esphome + +#endif diff --git a/esphome/const.py b/esphome/const.py index a30df6ef35..39578a1fcf 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -21,6 +21,7 @@ class Platform(StrEnum): HOST = "host" LIBRETINY_OLDSTYLE = "libretiny" LN882X = "ln882x" + NRF52 = "nrf52" RP2040 = "rp2040" RTL87XX = "rtl87xx" @@ -31,6 +32,7 @@ class Framework(StrEnum): ARDUINO = "arduino" ESP_IDF = "esp-idf" NATIVE = "host" + ZEPHYR = "zephyr" class PlatformFramework(Enum): @@ -47,6 +49,9 @@ class PlatformFramework(Enum): RTL87XX_ARDUINO = (Platform.RTL87XX, Framework.ARDUINO) LN882X_ARDUINO = (Platform.LN882X, Framework.ARDUINO) + # Zephyr framework platforms + NRF52_ZEPHYR = (Platform.NRF52, Framework.ZEPHYR) + # Host platform (native) HOST_NATIVE = (Platform.HOST, Framework.NATIVE) @@ -58,6 +63,7 @@ PLATFORM_ESP8266 = Platform.ESP8266 PLATFORM_HOST = Platform.HOST PLATFORM_LIBRETINY_OLDSTYLE = Platform.LIBRETINY_OLDSTYLE PLATFORM_LN882X = Platform.LN882X +PLATFORM_NRF52 = Platform.NRF52 PLATFORM_RP2040 = Platform.RP2040 PLATFORM_RTL87XX = Platform.RTL87XX @@ -90,6 +96,7 @@ CONF_ALL = "all" CONF_ALLOW_OTHER_USES = "allow_other_uses" CONF_ALPHA = "alpha" CONF_ALTITUDE = "altitude" +CONF_ALTITUDE_COMPENSATION = "altitude_compensation" CONF_AMBIENT_LIGHT = "ambient_light" CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source" @@ -915,6 +922,7 @@ CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic" CONF_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic" CONF_SWING_OFF_ACTION = "swing_off_action" CONF_SWING_VERTICAL_ACTION = "swing_vertical_action" +CONF_SWITCH = "switch" CONF_SWITCH_DATAPOINT = "switch_datapoint" CONF_SWITCHES = "switches" CONF_SYNC = "sync" @@ -1187,6 +1195,7 @@ UNIT_WATT = "W" UNIT_WATT_HOURS = "Wh" # device classes +DEVICE_CLASS_ABSOLUTE_HUMIDITY = "absolute_humidity" DEVICE_CLASS_APPARENT_POWER = "apparent_power" DEVICE_CLASS_AQI = "aqi" DEVICE_CLASS_AREA = "area" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index e33bbcf726..5ce2ed5caf 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -21,6 +21,7 @@ from esphome.const import ( PLATFORM_ESP8266, PLATFORM_HOST, PLATFORM_LN882X, + PLATFORM_NRF52, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -670,6 +671,10 @@ class EsphomeCore: def is_libretiny(self): return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x + @property + def is_nrf52(self): + return self.target_platform == PLATFORM_NRF52 + @property def is_host(self): return self.target_platform == PLATFORM_HOST @@ -686,6 +691,10 @@ class EsphomeCore: def using_esp_idf(self): return self.target_framework == "esp-idf" + @property + def using_zephyr(self): + return self.target_framework == "zephyr" + def add_job(self, func, *args, **kwargs) -> None: self.event_loop.add_job(func, *args, **kwargs) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index e19acd3ba6..748c8f2237 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -4,6 +4,9 @@ #include "esphome/core/hal.h" #include #include +#ifdef USE_RUNTIME_STATS +#include "esphome/components/runtime_stats/runtime_stats.h" +#endif #ifdef USE_STATUS_LED #include "esphome/components/status_led/status_led.h" @@ -68,7 +71,7 @@ void Application::setup() { do { uint8_t new_app_state = STATUS_LED_WARNING; - this->scheduler.call(); + this->scheduler.call(millis()); this->feed_wdt(); for (uint32_t j = 0; j <= i; j++) { // Update loop_component_start_time_ right before calling each component @@ -94,11 +97,11 @@ void Application::setup() { void Application::loop() { uint8_t new_app_state = 0; - this->scheduler.call(); - // Get the initial loop time at the start uint32_t last_op_end_time = millis(); + this->scheduler.call(last_op_end_time); + // Feed WDT with time this->feed_wdt(last_op_end_time); @@ -141,6 +144,14 @@ void Application::loop() { this->in_loop_ = false; this->app_state_ = new_app_state; +#ifdef USE_RUNTIME_STATS + // Process any pending runtime stats printing after all components have run + // This ensures stats printing doesn't affect component timing measurements + if (global_runtime_stats != nullptr) { + global_runtime_stats->process_pending_stats(last_op_end_time); + } +#endif + // Use the last component's end time instead of calling millis() again auto elapsed = last_op_end_time - this->last_loop_; if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { @@ -149,7 +160,7 @@ void Application::loop() { this->yield_with_select_(0); } else { uint32_t delay_time = this->loop_interval_ - elapsed; - uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time); + uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time); // next_schedule is max 0.5*delay_time // otherwise interval=0 schedules result in constant looping with almost no sleep next_schedule = std::max(next_schedule, delay_time / 2); diff --git a/esphome/core/application.h b/esphome/core/application.h index f2b5cb5c89..75b9769ca3 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -368,8 +368,19 @@ class Application { uint8_t get_app_state() const { return this->app_state_; } -// Helper macro for entity getter method declarations - reduces code duplication -// When USE_DEVICE_ID is enabled in the future, this can be conditionally compiled to add device_id parameter +// Helper macro for entity getter method declarations +#ifdef USE_DEVICES +#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \ + entity_type *get_##entity_name##_by_key(uint32_t key, uint32_t device_id, bool include_internal = false) { \ + for (auto *obj : this->entities_member##_) { \ + if (obj->get_object_id_hash() == key && obj->get_device_id() == device_id && \ + (include_internal || !obj->is_internal())) \ + return obj; \ + } \ + return nullptr; \ + } + const std::vector &get_devices() { return this->devices_; } +#else #define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \ entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \ for (auto *obj : this->entities_member##_) { \ @@ -378,10 +389,7 @@ class Application { } \ return nullptr; \ } - -#ifdef USE_DEVICES - const std::vector &get_devices() { return this->devices_; } -#endif +#endif // USE_DEVICES #ifdef USE_AREAS const std::vector &get_areas() { return this->areas_; } #endif diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 13179b90bb..740e10700b 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -158,14 +158,14 @@ template class DelayAction : public Action, public Compon void play_complex(Ts... x) override { auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; - this->set_timeout(this->delay_.value(x...), f); + this->set_timeout("delay", this->delay_.value(x...), f); } float get_setup_priority() const override { return setup_priority::HARDWARE; } void play(Ts... x) override { /* ignore - see play_complex */ } - void stop() override { this->cancel_timeout(""); } + void stop() override { this->cancel_timeout("delay"); } }; template class LambdaAction : public Action { diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index c47f16b5f7..aec6c17786 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -9,6 +9,9 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_RUNTIME_STATS +#include "esphome/components/runtime_stats/runtime_stats.h" +#endif namespace esphome { @@ -252,10 +255,10 @@ void Component::defer(const char *name, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, name, 0, std::move(f)); } void Component::set_timeout(uint32_t timeout, std::function &&f) { // NOLINT - App.scheduler.set_timeout(this, "", timeout, std::move(f)); + App.scheduler.set_timeout(this, static_cast(nullptr), timeout, std::move(f)); } void Component::set_interval(uint32_t interval, std::function &&f) { // NOLINT - App.scheduler.set_interval(this, "", interval, std::move(f)); + App.scheduler.set_interval(this, static_cast(nullptr), interval, std::move(f)); } void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, float backoff_increase_factor) { // NOLINT @@ -396,6 +399,13 @@ uint32_t WarnIfComponentBlockingGuard::finish() { uint32_t curr_time = millis(); uint32_t blocking_time = curr_time - this->started_; + +#ifdef USE_RUNTIME_STATS + // Record component runtime stats + if (global_runtime_stats != nullptr) { + global_runtime_stats->record_component_time(this->component_, blocking_time, curr_time); + } +#endif bool should_warn; if (this->component_ != nullptr) { should_warn = this->component_->should_warn_of_blocking(blocking_time); diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index d27c4e70ba..1e8f670d8b 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -16,373 +16,186 @@ void ComponentIterator::begin(bool include_internal) { this->at_ = 0; this->include_internal_ = include_internal; } + +template +void ComponentIterator::process_platform_item_(const std::vector &items, + bool (ComponentIterator::*on_item)(PlatformItem *)) { + if (this->at_ >= items.size()) { + this->advance_platform_(); + } else { + PlatformItem *item = items[this->at_]; + if ((item->is_internal() && !this->include_internal_) || (this->*on_item)(item)) { + this->at_++; + } + } +} + +void ComponentIterator::advance_platform_() { + this->state_ = static_cast(static_cast(this->state_) + 1); + this->at_ = 0; +} + void ComponentIterator::advance() { - bool advance_platform = false; - bool success = true; switch (this->state_) { case IteratorState::NONE: // not started return; case IteratorState::BEGIN: if (this->on_begin()) { - advance_platform = true; - } else { - return; + advance_platform_(); } break; + #ifdef USE_BINARY_SENSOR case IteratorState::BINARY_SENSOR: - if (this->at_ >= App.get_binary_sensors().size()) { - advance_platform = true; - } else { - auto *binary_sensor = App.get_binary_sensors()[this->at_]; - if (binary_sensor->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_binary_sensor(binary_sensor); - } - } + this->process_platform_item_(App.get_binary_sensors(), &ComponentIterator::on_binary_sensor); break; #endif + #ifdef USE_COVER case IteratorState::COVER: - if (this->at_ >= App.get_covers().size()) { - advance_platform = true; - } else { - auto *cover = App.get_covers()[this->at_]; - if (cover->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_cover(cover); - } - } + this->process_platform_item_(App.get_covers(), &ComponentIterator::on_cover); break; #endif + #ifdef USE_FAN case IteratorState::FAN: - if (this->at_ >= App.get_fans().size()) { - advance_platform = true; - } else { - auto *fan = App.get_fans()[this->at_]; - if (fan->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_fan(fan); - } - } + this->process_platform_item_(App.get_fans(), &ComponentIterator::on_fan); break; #endif + #ifdef USE_LIGHT case IteratorState::LIGHT: - if (this->at_ >= App.get_lights().size()) { - advance_platform = true; - } else { - auto *light = App.get_lights()[this->at_]; - if (light->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_light(light); - } - } + this->process_platform_item_(App.get_lights(), &ComponentIterator::on_light); break; #endif + #ifdef USE_SENSOR case IteratorState::SENSOR: - if (this->at_ >= App.get_sensors().size()) { - advance_platform = true; - } else { - auto *sensor = App.get_sensors()[this->at_]; - if (sensor->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_sensor(sensor); - } - } + this->process_platform_item_(App.get_sensors(), &ComponentIterator::on_sensor); break; #endif + #ifdef USE_SWITCH case IteratorState::SWITCH: - if (this->at_ >= App.get_switches().size()) { - advance_platform = true; - } else { - auto *a_switch = App.get_switches()[this->at_]; - if (a_switch->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_switch(a_switch); - } - } + this->process_platform_item_(App.get_switches(), &ComponentIterator::on_switch); break; #endif + #ifdef USE_BUTTON case IteratorState::BUTTON: - if (this->at_ >= App.get_buttons().size()) { - advance_platform = true; - } else { - auto *button = App.get_buttons()[this->at_]; - if (button->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_button(button); - } - } + this->process_platform_item_(App.get_buttons(), &ComponentIterator::on_button); break; #endif + #ifdef USE_TEXT_SENSOR case IteratorState::TEXT_SENSOR: - if (this->at_ >= App.get_text_sensors().size()) { - advance_platform = true; - } else { - auto *text_sensor = App.get_text_sensors()[this->at_]; - if (text_sensor->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_text_sensor(text_sensor); - } - } + this->process_platform_item_(App.get_text_sensors(), &ComponentIterator::on_text_sensor); break; #endif + #ifdef USE_API_SERVICES - case IteratorState ::SERVICE: - if (this->at_ >= api::global_api_server->get_user_services().size()) { - advance_platform = true; - } else { - auto *service = api::global_api_server->get_user_services()[this->at_]; - success = this->on_service(service); - } + case IteratorState::SERVICE: + this->process_platform_item_(api::global_api_server->get_user_services(), &ComponentIterator::on_service); break; #endif + #ifdef USE_CAMERA - case IteratorState::CAMERA: - if (camera::Camera::instance() == nullptr) { - advance_platform = true; - } else { - if (camera::Camera::instance()->is_internal() && !this->include_internal_) { - advance_platform = success = true; - break; - } else { - advance_platform = success = this->on_camera(camera::Camera::instance()); - } + case IteratorState::CAMERA: { + camera::Camera *camera_instance = camera::Camera::instance(); + if (camera_instance != nullptr && (!camera_instance->is_internal() || this->include_internal_)) { + this->on_camera(camera_instance); } - break; + advance_platform_(); + } break; #endif + #ifdef USE_CLIMATE case IteratorState::CLIMATE: - if (this->at_ >= App.get_climates().size()) { - advance_platform = true; - } else { - auto *climate = App.get_climates()[this->at_]; - if (climate->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_climate(climate); - } - } + this->process_platform_item_(App.get_climates(), &ComponentIterator::on_climate); break; #endif + #ifdef USE_NUMBER case IteratorState::NUMBER: - if (this->at_ >= App.get_numbers().size()) { - advance_platform = true; - } else { - auto *number = App.get_numbers()[this->at_]; - if (number->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_number(number); - } - } + this->process_platform_item_(App.get_numbers(), &ComponentIterator::on_number); break; #endif + #ifdef USE_DATETIME_DATE case IteratorState::DATETIME_DATE: - if (this->at_ >= App.get_dates().size()) { - advance_platform = true; - } else { - auto *date = App.get_dates()[this->at_]; - if (date->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_date(date); - } - } + this->process_platform_item_(App.get_dates(), &ComponentIterator::on_date); break; #endif + #ifdef USE_DATETIME_TIME case IteratorState::DATETIME_TIME: - if (this->at_ >= App.get_times().size()) { - advance_platform = true; - } else { - auto *time = App.get_times()[this->at_]; - if (time->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_time(time); - } - } + this->process_platform_item_(App.get_times(), &ComponentIterator::on_time); break; #endif + #ifdef USE_DATETIME_DATETIME case IteratorState::DATETIME_DATETIME: - if (this->at_ >= App.get_datetimes().size()) { - advance_platform = true; - } else { - auto *datetime = App.get_datetimes()[this->at_]; - if (datetime->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_datetime(datetime); - } - } + this->process_platform_item_(App.get_datetimes(), &ComponentIterator::on_datetime); break; #endif + #ifdef USE_TEXT case IteratorState::TEXT: - if (this->at_ >= App.get_texts().size()) { - advance_platform = true; - } else { - auto *text = App.get_texts()[this->at_]; - if (text->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_text(text); - } - } + this->process_platform_item_(App.get_texts(), &ComponentIterator::on_text); break; #endif + #ifdef USE_SELECT case IteratorState::SELECT: - if (this->at_ >= App.get_selects().size()) { - advance_platform = true; - } else { - auto *select = App.get_selects()[this->at_]; - if (select->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_select(select); - } - } + this->process_platform_item_(App.get_selects(), &ComponentIterator::on_select); break; #endif + #ifdef USE_LOCK case IteratorState::LOCK: - if (this->at_ >= App.get_locks().size()) { - advance_platform = true; - } else { - auto *a_lock = App.get_locks()[this->at_]; - if (a_lock->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_lock(a_lock); - } - } + this->process_platform_item_(App.get_locks(), &ComponentIterator::on_lock); break; #endif + #ifdef USE_VALVE case IteratorState::VALVE: - if (this->at_ >= App.get_valves().size()) { - advance_platform = true; - } else { - auto *valve = App.get_valves()[this->at_]; - if (valve->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_valve(valve); - } - } + this->process_platform_item_(App.get_valves(), &ComponentIterator::on_valve); break; #endif + #ifdef USE_MEDIA_PLAYER case IteratorState::MEDIA_PLAYER: - if (this->at_ >= App.get_media_players().size()) { - advance_platform = true; - } else { - auto *media_player = App.get_media_players()[this->at_]; - if (media_player->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_media_player(media_player); - } - } + this->process_platform_item_(App.get_media_players(), &ComponentIterator::on_media_player); break; #endif + #ifdef USE_ALARM_CONTROL_PANEL case IteratorState::ALARM_CONTROL_PANEL: - if (this->at_ >= App.get_alarm_control_panels().size()) { - advance_platform = true; - } else { - auto *a_alarm_control_panel = App.get_alarm_control_panels()[this->at_]; - if (a_alarm_control_panel->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_alarm_control_panel(a_alarm_control_panel); - } - } + this->process_platform_item_(App.get_alarm_control_panels(), &ComponentIterator::on_alarm_control_panel); break; #endif + #ifdef USE_EVENT case IteratorState::EVENT: - if (this->at_ >= App.get_events().size()) { - advance_platform = true; - } else { - auto *event = App.get_events()[this->at_]; - if (event->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_event(event); - } - } + this->process_platform_item_(App.get_events(), &ComponentIterator::on_event); break; #endif + #ifdef USE_UPDATE case IteratorState::UPDATE: - if (this->at_ >= App.get_updates().size()) { - advance_platform = true; - } else { - auto *update = App.get_updates()[this->at_]; - if (update->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_update(update); - } - } + this->process_platform_item_(App.get_updates(), &ComponentIterator::on_update); break; #endif + case IteratorState::MAX: if (this->on_end()) { this->state_ = IteratorState::NONE; } return; } - - if (advance_platform) { - this->state_ = static_cast(static_cast(this->state_) + 1); - this->at_ = 0; - } else if (success) { - this->at_++; - } } + bool ComponentIterator::on_end() { return true; } bool ComponentIterator::on_begin() { return true; } #ifdef USE_API_SERVICES diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index ea2c8004ac..7a9771b8f2 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -171,6 +171,11 @@ class ComponentIterator { } state_{IteratorState::NONE}; uint16_t at_{0}; // Supports up to 65,535 entities per type bool include_internal_{false}; + + template + void process_platform_item_(const std::vector &items, + bool (ComponentIterator::*on_item)(PlatformItem *)); + void advance_platform_(); }; } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8ed8f4b5aa..7ddb3436cd 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -145,6 +145,7 @@ #define USE_CAPTIVE_PORTAL #define USE_ESP32_BLE #define USE_ESP32_BLE_CLIENT +#define USE_ESP32_BLE_DEVICE #define USE_ESP32_BLE_SERVER #define USE_I2C #define USE_IMPROV diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 5ad16ac76c..cc388ffb4c 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -198,9 +198,12 @@ 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_name = device_id_obj.id + # Use the device ID string directly for uniqueness + device_id = device_id_obj.id # Calculate what object_id will actually be used # This handles empty names correctly by using device/friendly names @@ -209,11 +212,12 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy ) # Check for duplicates - unique_key = (platform, name_key) + 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. " - f"Each entity must have a unique name within its platform across all devices." + 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." ) # Add to tracking set diff --git a/esphome/core/event_pool.h b/esphome/core/event_pool.h index 69e03bafac..928a4e7dee 100644 --- a/esphome/core/event_pool.h +++ b/esphome/core/event_pool.h @@ -1,6 +1,6 @@ #pragma once -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) #include #include @@ -78,4 +78,4 @@ template class EventPool { } // namespace esphome -#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) +#endif // defined(USE_ESP32) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c3b404ae60..260479c9e1 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -678,11 +679,28 @@ class InterruptLock { ~InterruptLock(); protected: -#if defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) uint32_t state_; #endif }; +/** Helper class to lock the lwIP TCPIP core when making lwIP API calls from non-TCPIP threads. + * + * This is needed on multi-threaded platforms (ESP32) when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled. + * It ensures thread-safe access to lwIP APIs. + * + * @note This follows the same pattern as InterruptLock - platform-specific implementations in helpers.cpp + */ +class LwIPLock { + public: + LwIPLock(); + ~LwIPLock(); + + // Delete copy constructor and copy assignment operator to prevent accidental copying + LwIPLock(const LwIPLock &) = delete; + LwIPLock &operator=(const LwIPLock &) = delete; +}; + /** Helper class to request `loop()` to be called as fast as possible. * * Usually the ESPHome main loop runs at 60 Hz, sleeping in between invocations of `loop()` if necessary. When a higher diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index f35cfa5af9..de07b0ebba 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -1,17 +1,12 @@ #pragma once -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) #include #include -#if defined(USE_ESP32) #include #include -#elif defined(USE_LIBRETINY) -#include -#include -#endif /* * Lock-free queue for single-producer single-consumer scenarios. @@ -148,4 +143,4 @@ template class NotifyingLockFreeQueue : public LockFreeQu } // namespace esphome -#endif // defined(USE_ESP32) || defined(USE_LIBRETINY) +#endif // defined(USE_ESP32) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index c6893b128f..7a0c08e1f0 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -8,12 +8,15 @@ #include #include #include +#include namespace esphome { static const char *const TAG = "scheduler"; static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; +// 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; // Uncomment to debug scheduler // #define ESPHOME_DEBUG_SCHEDULER @@ -91,7 +94,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #endif - const auto now = this->millis_(); + // 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) { @@ -193,9 +197,7 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor); if (backoff_increase_factor < 0.0001) { - ESP_LOGE(TAG, - "set_retry(name='%s'): backoff_factor cannot be close to zero nor negative (%0.1f). Using 1.0 instead", - name.c_str(), backoff_increase_factor); + ESP_LOGE(TAG, "backoff_factor %0.1f too small, using 1.0: %s", backoff_increase_factor, name.c_str()); backoff_increase_factor = 1; } @@ -215,19 +217,20 @@ bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) return this->cancel_timeout(component, "retry$" + name); } -optional HOT Scheduler::next_schedule_in() { +optional HOT Scheduler::next_schedule_in(uint32_t now) { // IMPORTANT: This method should only be called from the main thread (loop task). // It calls empty_() and accesses items_[0] without holding a lock, which is only // safe when called from the main thread. Other threads must not call this method. if (this->empty_()) return {}; auto &item = this->items_[0]; - const auto now = this->millis_(); - if (item->next_execution_ < now) + // 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) return 0; - return item->next_execution_ - now; + return item->next_execution_ - now_64; } -void HOT Scheduler::call() { +void HOT Scheduler::call(uint32_t now) { #if !defined(USE_ESP8266) && !defined(USE_RP2040) // Process defer queue first to guarantee FIFO execution order for deferred items. // Previously, defer() used the heap which gave undefined order for equal timestamps, @@ -256,22 +259,28 @@ void HOT Scheduler::call() { // 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()); + this->execute_item_(item.get(), now); } } #endif - const auto now = this->millis_(); + // 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(); #ifdef ESPHOME_DEBUG_SCHEDULER static uint64_t last_print = 0; - if (now - last_print > 2000) { - last_print = now; + if (now_64 - last_print > 2000) { + last_print = now_64; std::vector> old_items; - ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now, this->millis_major_, - this->last_millis_); +#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64, + this->millis_major_, this->last_millis_.load(std::memory_order_relaxed)); +#else + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%u, %" PRIu32 ")", this->items_.size(), now_64, + this->millis_major_, this->last_millis_); +#endif while (!this->empty_()) { std::unique_ptr item; { @@ -283,7 +292,7 @@ void HOT Scheduler::call() { 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, item->next_execution_); + item->next_execution_ - now_64, item->next_execution_); old_items.push_back(std::move(item)); } @@ -328,7 +337,7 @@ void HOT Scheduler::call() { { // Don't copy-by value yet auto &item = this->items_[0]; - if (item->next_execution_ > now) { + if (item->next_execution_ > now_64) { // Not reached timeout yet, done for this call break; } @@ -342,13 +351,13 @@ void HOT Scheduler::call() { 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); + item->next_execution_, now_64); #endif // 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()); + this->execute_item_(item.get(), now); } { @@ -367,7 +376,7 @@ void HOT Scheduler::call() { } if (item->type == SchedulerItem::INTERVAL) { - item->next_execution_ = now + item->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)); @@ -423,11 +432,9 @@ void HOT Scheduler::pop_raw_() { } // Helper to execute a scheduler item -void HOT Scheduler::execute_item_(SchedulerItem *item) { +void HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) { App.set_current_component(item->component); - - uint32_t now_ms = millis(); - WarnIfComponentBlockingGuard guard{item->component, now_ms}; + WarnIfComponentBlockingGuard guard{item->component, now}; item->callback(); guard.finish(); } @@ -446,7 +453,7 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co // Helper to cancel items by name - must be called with lock held bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) { // Early return if name is invalid - no items to cancel - if (name_cstr == nullptr || name_cstr[0] == '\0') { + if (name_cstr == nullptr) { return false; } @@ -486,17 +493,112 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c return total_cancelled > 0; } -uint64_t Scheduler::millis_() { - // Get the current 32-bit millis value - const uint32_t now = millis(); - // Check for rollover by comparing with last value - if (now < this->last_millis_) { - // Detected rollover (happens every ~49.7 days) - this->millis_major_++; - ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms", - now + (static_cast(this->millis_major_) << 32)); +uint64_t Scheduler::millis_64_(uint32_t now) { + // THREAD SAFETY NOTE: + // This function can be called from multiple threads simultaneously on ESP32/LibreTiny. + // On single-threaded platforms (ESP8266, RP2040), atomics are not needed. + // + // IMPORTANT: Always pass fresh millis() values to this function. The implementation + // handles out-of-order timestamps between threads, but minimizing time differences + // helps maintain accuracy. + // + // The implementation handles the 32-bit rollover (every 49.7 days) by: + // 1. Using a lock when detecting rollover to ensure atomic update + // 2. Restricting normal updates to forward movement within the same epoch + // This prevents race conditions at the rollover boundary without requiring + // 64-bit atomics or locking on every call. + +#ifdef USE_LIBRETINY + // LibreTiny: Multi-threaded but lacks atomic operation support + // TODO: If LibreTiny ever adds atomic support, remove this entire block and + // let it fall through to the atomic-based implementation below + // We need to use a lock when near the rollover boundary to prevent races + uint32_t last = this->last_millis_; + + // Define a safe window around the rollover point (10 seconds) + // This covers any reasonable scheduler delays or thread preemption + static const uint32_t ROLLOVER_WINDOW = 10000; // 10 seconds in milliseconds + + // Check if we're near the rollover boundary (close to std::numeric_limits::max() or just past 0) + bool near_rollover = (last > (std::numeric_limits::max() - ROLLOVER_WINDOW)) || (now < ROLLOVER_WINDOW); + + if (near_rollover || (now < last && (last - now) > HALF_MAX_UINT32)) { + // Near rollover or detected a rollover - need lock for safety + LockGuard guard{this->lock_}; + // Re-read with lock held + last = this->last_millis_; + + if (now < last && (last - now) > HALF_MAX_UINT32) { + // True rollover detected (happens every ~49.7 days) + this->millis_major_++; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif + } + // Update last_millis_ while holding lock + this->last_millis_ = now; + } else if (now > last) { + // Normal case: Not near rollover and time moved forward + // Update without lock. While this may cause minor races (microseconds of + // backwards time movement), they're acceptable because: + // 1. The scheduler operates at millisecond resolution, not microsecond + // 2. We've already prevented the critical rollover race condition + // 3. Any backwards movement is orders of magnitude smaller than scheduler delays + this->last_millis_ = now; } - this->last_millis_ = now; + // If now <= last and we're not near rollover, don't update + // This minimizes backwards time movement + +#elif !defined(USE_ESP8266) && !defined(USE_RP2040) + // Multi-threaded platforms with atomic support (ESP32) + uint32_t last = this->last_millis_.load(std::memory_order_relaxed); + + // If we might be near a rollover (large backwards jump), take the lock for the entire operation + // This ensures rollover detection and last_millis_ update are atomic together + if (now < last && (last - now) > HALF_MAX_UINT32) { + // Potential rollover - need lock for atomic rollover detection + update + LockGuard guard{this->lock_}; + // Re-read with lock held + last = this->last_millis_.load(std::memory_order_relaxed); + + if (now < last && (last - now) > HALF_MAX_UINT32) { + // True rollover detected (happens every ~49.7 days) + this->millis_major_++; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif + } + // Update last_millis_ while holding lock to prevent races + this->last_millis_.store(now, std::memory_order_relaxed); + } else { + // Normal case: Try lock-free update, but only allow forward movement within same epoch + // This prevents accidentally moving backwards across a rollover boundary + while (now > last && (now - last) < HALF_MAX_UINT32) { + if (this->last_millis_.compare_exchange_weak(last, now, std::memory_order_relaxed)) { + break; + } + // last is automatically updated by compare_exchange_weak if it fails + } + } + +#else + // Single-threaded platforms (ESP8266, RP2040): No atomics needed + uint32_t last = this->last_millis_; + + // Check for rollover + if (now < last && (last - now) > HALF_MAX_UINT32) { + this->millis_major_++; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif + } + + // Only update if time moved forward + if (now > last) { + this->last_millis_ = now; + } +#endif + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time return now + (static_cast(this->millis_major_) << 32); } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 39cee5a876..64df2f2bb0 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -4,6 +4,9 @@ #include #include #include +#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) +#include +#endif #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -52,9 +55,13 @@ class Scheduler { std::function func, float backoff_increase_factor = 1.0f); bool cancel_retry(Component *component, const std::string &name); - optional next_schedule_in(); + // Calculate when the next scheduled item should run + // @param now Fresh timestamp from millis() - must not be stale/cached + optional next_schedule_in(uint32_t now); - void call(); + // Execute all scheduled items that are ready + // @param now Fresh timestamp from millis() - must not be stale/cached + void call(uint32_t now); void process_to_add(); @@ -114,16 +121,17 @@ class Scheduler { name_is_dynamic = false; } - if (!name || !name[0]) { + if (!name) { + // nullptr case - no name provided name_.static_name = nullptr; } else if (make_copy) { - // Make a copy for dynamic strings + // Make a copy for dynamic strings (including empty strings) size_t len = strlen(name); name_.dynamic_name = new char[len + 1]; memcpy(name_.dynamic_name, name, len + 1); name_is_dynamic = true; } else { - // Use static string directly + // Use static string directly (including empty strings) name_.static_name = name; } } @@ -137,7 +145,7 @@ class Scheduler { void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, uint32_t delay, std::function func); - uint64_t millis_(); + uint64_t millis_64_(uint32_t now); void cleanup_(); void pop_raw_(); @@ -175,7 +183,7 @@ class Scheduler { } // Helper to execute a scheduler item - void execute_item_(SchedulerItem *item); + void execute_item_(SchedulerItem *item, uint32_t now); // Helper to check if item should be skipped bool should_skip_item_(const SchedulerItem *item) const { @@ -203,7 +211,14 @@ class Scheduler { // Both platforms save 40 bytes of RAM by excluding this std::deque> defer_queue_; // FIFO queue for defer() calls #endif +#if !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) + // Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates + std::atomic last_millis_{0}; +#else + // Platforms without atomic support or single-threaded platforms uint32_t last_millis_{0}; +#endif + // millis_major_ is protected by lock when incrementing uint16_t millis_major_{0}; uint32_t to_remove_{0}; }; diff --git a/esphome/dashboard/dns.py b/esphome/dashboard/dns.py index ea85d338bf..98134062f4 100644 --- a/esphome/dashboard/dns.py +++ b/esphome/dashboard/dns.py @@ -3,15 +3,9 @@ from __future__ import annotations import asyncio from contextlib import suppress from ipaddress import ip_address -import sys from icmplib import NameLookupError, async_resolve -if sys.version_info >= (3, 11): - from asyncio import timeout as async_timeout -else: - from async_timeout import timeout as async_timeout - RESOLVE_TIMEOUT = 3.0 @@ -20,9 +14,9 @@ async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception: with suppress(ValueError): return [str(ip_address(hostname))] try: - async with async_timeout(RESOLVE_TIMEOUT): + async with asyncio.timeout(RESOLVE_TIMEOUT): return await async_resolve(hostname) - except (asyncio.TimeoutError, NameLookupError, UnicodeError) as ex: + except (TimeoutError, NameLookupError, UnicodeError) as ex: return ex diff --git a/esphome/util.py b/esphome/util.py index ba26b8adc1..79cb630200 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -147,6 +147,13 @@ class RedirectText: continue self._write_color_replace(line) + # Check for flash size error and provide helpful guidance + if ( + "Error: The program size" in line + and "is greater than maximum allowed" in line + and (help_msg := get_esp32_arduino_flash_error_help()) + ): + self._write_color_replace(help_msg) else: self._write_color_replace(s) @@ -309,3 +316,34 @@ def get_serial_ports() -> list[SerialPort]: result.sort(key=lambda x: x.path) return result + + +def get_esp32_arduino_flash_error_help() -> str | None: + """Returns helpful message when ESP32 with Arduino runs out of flash space.""" + from esphome.core import CORE + + if not (CORE.is_esp32 and CORE.using_arduino): + return None + + from esphome.log import AnsiFore, color + + return ( + "\n" + + color( + AnsiFore.YELLOW, + "💡 TIP: Your ESP32 with Arduino framework has run out of flash space.\n", + ) + + "\n" + + "To fix this, switch to the ESP-IDF framework which is more memory efficient:\n" + + "\n" + + "1. In your YAML configuration, modify the framework section:\n" + + "\n" + + " esp32:\n" + + " framework:\n" + + " type: esp-idf\n" + + "\n" + + "2. Clean build files and compile again\n" + + "\n" + + "Note: ESP-IDF uses less flash space and provides better performance.\n" + + "Some Arduino-specific libraries may need alternatives.\n\n" + ) diff --git a/platformio.ini b/platformio.ini index 8fcc578103..7fb301c08b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -138,7 +138,7 @@ lib_deps = WiFi ; wifi,web_server_base,ethernet (Arduino built-in) Update ; ota,web_server_base (Arduino built-in) ${common:arduino.lib_deps} - ESP32Async/AsyncTCP@3.4.4 ; async_tcp + ESP32Async/AsyncTCP@3.4.5 ; async_tcp NetworkClientSecure ; http_request,nextion (Arduino built-in) HTTPClient ; http_request,nextion (Arduino built-in) ESPmDNS ; mdns (Arduino built-in) diff --git a/pyproject.toml b/pyproject.toml index 97b0df9eff..5d48779ad5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,15 +20,15 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Home Automation", ] -requires-python = ">=3.10.0" +requires-python = ">=3.11.0" dynamic = ["dependencies", "optional-dependencies", "version"] [project.urls] "Documentation" = "https://esphome.io" "Source Code" = "https://github.com/esphome/esphome" -"Bug Tracker" = "https://github.com/esphome/issues/issues" -"Feature Request Tracker" = "https://github.com/esphome/feature-requests/issues" +"Bug Tracker" = "https://github.com/esphome/esphome/issues" +"Feature Request Tracker" = "https://github.com/orgs/esphome/discussions" "Discord" = "https://discord.gg/KhAMKrd" "Forum" = "https://community.home-assistant.io/c/esphome" "Twitter" = "https://twitter.com/esphome_" @@ -62,7 +62,7 @@ addopts = [ ] [tool.pylint.MAIN] -py-version = "3.10" +py-version = "3.11" ignore = [ "api_pb2.py", ] @@ -106,7 +106,7 @@ expected-line-ending-format = "LF" [tool.ruff] required-version = ">=0.5.0" -target-version = "py310" +target-version = "py311" exclude = ['generated'] [tool.ruff.lint] diff --git a/requirements.txt b/requirements.txt index 8829208f30..4d94ce5557 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -async_timeout==5.0.1; python_version <= "3.10" cryptography==45.0.1 voluptuous==0.15.2 PyYAML==6.0.2 @@ -13,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==35.0.1 +aioesphomeapi==37.0.1 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.14 # dashboard_import diff --git a/requirements_test.txt b/requirements_test.txt index 67eae63a31..ad5e4a3e3d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.7 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.3 # also change in .pre-commit-config.yaml when updating +ruff==0.12.4 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit @@ -8,7 +8,7 @@ pre-commit pytest==8.4.1 pytest-cov==6.2.1 pytest-mock==3.14.1 -pytest-asyncio==1.0.0 -pytest-xdist==3.7.0 +pytest-asyncio==1.1.0 +pytest-xdist==3.8.0 asyncmock==0.4.2 hypothesis==6.92.1 diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 3ae1b195e4..4df7692167 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -8,7 +8,6 @@ from pathlib import Path import re from subprocess import call import sys -from textwrap import dedent from typing import Any import aioesphomeapi.api_options_pb2 as pb @@ -76,6 +75,30 @@ def indent(text: str, padding: str = " ") -> str: return "\n".join(indent_list(text, padding)) +def wrap_with_ifdef(content: str | list[str], ifdef: str | None) -> list[str]: + """Wrap content with #ifdef directives if ifdef is provided. + + Args: + content: Single string or list of strings to wrap + ifdef: The ifdef condition, or None to skip wrapping + + Returns: + List of strings with ifdef wrapping if needed + """ + if not ifdef: + if isinstance(content, str): + return [content] + return content + + result = [f"#ifdef {ifdef}"] + if isinstance(content, str): + result.append(content) + else: + result.extend(content) + result.append("#endif") + return result + + def camel_to_snake(name: str) -> str: # https://stackoverflow.com/a/1176023 s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) @@ -157,13 +180,7 @@ class TypeInfo(ABC): content = self.decode_varint if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name} = {content}; - return true; - }}""" - ) + return f"case {self.number}: this->{self.field_name} = {content}; break;" decode_varint = None @@ -172,13 +189,7 @@ class TypeInfo(ABC): content = self.decode_length if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name} = {content}; - return true; - }}""" - ) + return f"case {self.number}: this->{self.field_name} = {content}; break;" decode_length = None @@ -187,13 +198,7 @@ class TypeInfo(ABC): content = self.decode_32bit if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name} = {content}; - return true; - }}""" - ) + return f"case {self.number}: this->{self.field_name} = {content}; break;" decode_32bit = None @@ -202,13 +207,7 @@ class TypeInfo(ABC): content = self.decode_64bit if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name} = {content}; - return true; - }}""" - ) + return f"case {self.number}: this->{self.field_name} = {content}; break;" decode_64bit = None @@ -265,26 +264,6 @@ class TypeInfo(ABC): value = value_expr if value_expr else name return f"ProtoSize::{method}(total_size, {field_id_size}, {value});" - def _get_fixed_size_calculation( - self, name: str, force: bool, num_bytes: int, zero_check: str - ) -> str: - """Helper for fixed-size field calculations. - - Args: - name: Field name - force: Whether this is for a repeated field - num_bytes: Number of bytes (4 or 8) - zero_check: Expression to check for zero value (e.g., "!= 0.0f") - """ - field_id_size = self.calculate_field_id_size() - # Fixed-size repeated fields are handled differently in RepeatedTypeInfo - # so we should never get force=True here - assert not force, ( - "Fixed-size repeated fields should be handled by RepeatedTypeInfo" - ) - method = f"add_fixed_field<{num_bytes}>" - return f"ProtoSize::{method}(total_size, {field_id_size}, {name} {zero_check});" - @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: """Calculate the size needed for encoding this field. @@ -313,6 +292,42 @@ class TypeInfo(ABC): TYPE_INFO: dict[int, TypeInfo] = {} +# Unsupported 64-bit types that would add overhead for embedded systems +# TYPE_DOUBLE = 1, TYPE_FIXED64 = 6, TYPE_SFIXED64 = 16, TYPE_SINT64 = 18 +UNSUPPORTED_TYPES = {1: "double", 6: "fixed64", 16: "sfixed64", 18: "sint64"} + + +def validate_field_type(field_type: int, field_name: str = "") -> None: + """Validate that the field type is supported by ESPHome API. + + Raises ValueError for unsupported 64-bit types. + """ + if field_type in UNSUPPORTED_TYPES: + type_name = UNSUPPORTED_TYPES[field_type] + field_info = f" (field: {field_name})" if field_name else "" + raise ValueError( + f"64-bit type '{type_name}'{field_info} is not supported by ESPHome API. " + "These types add significant overhead for embedded systems. " + "If you need 64-bit support, please add the necessary encoding/decoding " + "functions to proto.h/proto.cpp first." + ) + + +def create_field_type_info(field: descriptor.FieldDescriptorProto) -> TypeInfo: + """Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options.""" + if field.label == 3: # repeated + 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) + + validate_field_type(field.type, field.name) + return TYPE_INFO[field.type](field) + def register_type(name: int): """Decorator to register a type with a name and number.""" @@ -339,7 +354,8 @@ class DoubleType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 8, "!= 0.0") + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_double_field(total_size, {field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -362,7 +378,8 @@ class FloatType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 4, "!= 0.0f") + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_float_field(total_size, {field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -445,7 +462,8 @@ class Fixed64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 8, "!= 0") + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_fixed64_field(total_size, {field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -468,7 +486,8 @@ class Fixed32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 4, "!= 0") + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_fixed32_field(total_size, {field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -549,13 +568,7 @@ class MessageType(TypeInfo): @property def decode_length_content(self) -> str: # Custom decode that doesn't use templates - return dedent( - f"""\ - case {self.number}: {{ - value.decode_to_message(this->{self.field_name}); - return true; - }}""" - ) + return f"case {self.number}: value.decode_to_message(this->{self.field_name}); break;" def dump(self, name: str) -> str: o = f"{name}.dump_to(out);" @@ -595,6 +608,85 @@ class BytesType(TypeInfo): return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes +class FixedArrayBytesType(TypeInfo): + """Special type for fixed-size byte arrays.""" + + def __init__(self, field: descriptor.FieldDescriptorProto, size: int) -> None: + super().__init__(field) + self.array_size = size + + @property + def cpp_type(self) -> str: + return "uint8_t" + + @property + def default_value(self) -> str: + return "{}" + + @property + def reference_type(self) -> str: + return f"uint8_t (&)[{self.array_size}]" + + @property + def const_reference_type(self) -> str: + return f"const uint8_t (&)[{self.array_size}]" + + @property + def public_content(self) -> list[str]: + # 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}};", + ] + + @property + def decode_length_content(self) -> str: + o = f"case {self.number}: {{\n" + o += " const std::string &data_str = value.as_string();\n" + o += f" this->{self.field_name}_len = data_str.size();\n" + o += f" if (this->{self.field_name}_len > {self.array_size}) {{\n" + o += f" this->{self.field_name}_len = {self.array_size};\n" + o += " }\n" + o += f" memcpy(this->{self.field_name}, data_str.data(), this->{self.field_name}_len);\n" + o += " break;\n" + o += "}" + return o + + @property + def encode_content(self) -> str: + return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" + + def dump(self, name: str) -> str: + o = f"out.append(format_hex_pretty({name}, {name}_len));" + return o + + def get_size_calculation(self, name: str, force: bool = False) -> str: + # Use the actual length stored in the _len field + length_field = f"this->{self.field_name}_len" + field_id_size = self.calculate_field_id_size() + + if force: + # For repeated fields, always calculate size + return f"total_size += {field_id_size} + ProtoSize::varint(static_cast({length_field})) + {length_field};" + else: + # For non-repeated fields, skip if length is 0 (matching encode_string behavior) + return ( + f"if ({length_field} != 0) {{\n" + f" total_size += {field_id_size} + ProtoSize::varint(static_cast({length_field})) + {length_field};\n" + f"}}" + ) + + def get_estimated_size(self) -> int: + # Estimate based on typical BLE advertisement size + return ( + self.calculate_field_id_size() + 1 + 31 + ) # field ID + length byte + typical 31 bytes + + @property + def wire_type(self) -> WireType: + return WireType.LENGTH_DELIMITED + + @register_type(13) class UInt32Type(TypeInfo): cpp_type = "uint32_t" @@ -663,7 +755,8 @@ class SFixed32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 4, "!= 0") + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_sfixed32_field(total_size, {field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -686,7 +779,8 @@ class SFixed64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 8, "!= 0") + field_id_size = self.calculate_field_id_size() + return f"ProtoSize::add_sfixed64_field(total_size, {field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -738,6 +832,17 @@ class SInt64Type(TypeInfo): class RepeatedTypeInfo(TypeInfo): def __init__(self, field: descriptor.FieldDescriptorProto) -> None: super().__init__(field) + # 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 + # So we extract just the type creation logic + if ( + field.type == 12 + and (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None + ): + self._ti: TypeInfo = FixedArrayBytesType(field, fixed_size) + return + + validate_field_type(field.type, field.name) self._ti: TypeInfo = TYPE_INFO[field.type](field) @property @@ -765,12 +870,8 @@ class RepeatedTypeInfo(TypeInfo): content = self._ti.decode_varint if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name}.push_back({content}); - return true; - }}""" + return ( + f"case {self.number}: this->{self.field_name}.push_back({content}); break;" ) @property @@ -778,22 +879,11 @@ class RepeatedTypeInfo(TypeInfo): content = self._ti.decode_length if content is None and isinstance(self._ti, MessageType): # Special handling for non-template message decoding - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name}.emplace_back(); - value.decode_to_message(this->{self.field_name}.back()); - return true; - }}""" - ) + return f"case {self.number}: this->{self.field_name}.emplace_back(); value.decode_to_message(this->{self.field_name}.back()); break;" if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name}.push_back({content}); - return true; - }}""" + return ( + f"case {self.number}: this->{self.field_name}.push_back({content}); break;" ) @property @@ -801,12 +891,8 @@ class RepeatedTypeInfo(TypeInfo): content = self._ti.decode_32bit if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name}.push_back({content}); - return true; - }}""" + return ( + f"case {self.number}: this->{self.field_name}.push_back({content}); break;" ) @property @@ -814,12 +900,8 @@ class RepeatedTypeInfo(TypeInfo): content = self._ti.decode_64bit if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name}.push_back({content}); - return true; - }}""" + return ( + f"case {self.number}: this->{self.field_name}.push_back({content}); break;" ) @property @@ -889,14 +971,15 @@ class RepeatedTypeInfo(TypeInfo): def build_type_usage_map( file_desc: descriptor.FileDescriptorProto, -) -> tuple[dict[str, str | None], dict[str, str | None]]: +) -> tuple[dict[str, str | None], dict[str, str | None], dict[str, int]]: """Build mappings for both enums and messages to their ifdefs based on usage. Returns: - tuple: (enum_ifdef_map, message_ifdef_map) + tuple: (enum_ifdef_map, message_ifdef_map, message_source_map) """ enum_ifdef_map: dict[str, str | None] = {} message_ifdef_map: dict[str, str | None] = {} + message_source_map: dict[str, int] = {} # Build maps of which types are used by which messages enum_usage: dict[ @@ -983,7 +1066,44 @@ def build_type_usage_map( message_ifdef_map[message.name] = parent_ifdefs.pop() changed = True - return enum_ifdef_map, message_ifdef_map + # Build message source map + # First pass: Get explicit sources for messages with source option or id + for msg in file_desc.message_type: + if msg.options.HasExtension(pb.source): + # Explicit source option takes precedence + message_source_map[msg.name] = get_opt(msg, pb.source, SOURCE_BOTH) + elif msg.options.HasExtension(pb.id): + # Service messages (with id) default to SOURCE_BOTH + message_source_map[msg.name] = SOURCE_BOTH + + # Second pass: Determine sources for embedded messages based on their usage + for msg in file_desc.message_type: + if msg.name in message_source_map: + continue # Already has explicit source + + if msg.name in message_usage: + # Get sources from all parent messages that use this one + parent_sources = { + message_source_map[parent] + for parent in message_usage[msg.name] + if parent in message_source_map + } + + # Combine parent sources + if not parent_sources: + # No parent has explicit source, default to encode-only + message_source_map[msg.name] = SOURCE_SERVER + elif len(parent_sources) > 1: + # Multiple different sources or SOURCE_BOTH present + message_source_map[msg.name] = SOURCE_BOTH + else: + # Inherit single parent source + message_source_map[msg.name] = parent_sources.pop() + else: + # Not used by any message and no explicit source - default to encode-only + message_source_map[msg.name] = SOURCE_SERVER + + return enum_ifdef_map, message_ifdef_map, message_source_map def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]: @@ -1025,10 +1145,7 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int: total_size = 0 for field in desc.field: - if field.label == 3: # repeated - ti = RepeatedTypeInfo(field) - else: - ti = TYPE_INFO[field.type](field) + ti = create_field_type_info(field) # Add estimated size for this field total_size += ti.get_estimated_size() @@ -1038,7 +1155,8 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int: def build_message_type( desc: descriptor.DescriptorProto, - base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]] = None, + base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]], + message_source_map: dict[str, int], ) -> tuple[str, str, str]: public_content: list[str] = [] protected_content: list[str] = [] @@ -1060,7 +1178,7 @@ def build_message_type( message_id: int | None = get_opt(desc, pb.id) # Get source direction to determine if we need decode/encode methods - source: int = get_opt(desc, pb.source, SOURCE_BOTH) + source = message_source_map[desc.name] needs_decode = source in (SOURCE_BOTH, SOURCE_CLIENT) needs_encode = source in (SOURCE_BOTH, SOURCE_SERVER) @@ -1095,72 +1213,109 @@ def build_message_type( public_content.append("#endif") for field in desc.field: - if field.label == 3: - ti = RepeatedTypeInfo(field) - else: - ti = TYPE_INFO[field.type](field) + ti = create_field_type_info(field) # Skip field declarations for fields that are in the base class # but include their encode/decode logic if field.name not in common_field_names: - protected_content.extend(ti.protected_content) - public_content.extend(ti.public_content) + # Check for field_ifdef option + field_ifdef = None + if field.options.HasExtension(pb.field_ifdef): + field_ifdef = field.options.Extensions[pb.field_ifdef] + + if ti.protected_content: + protected_content.extend( + wrap_with_ifdef(ti.protected_content, field_ifdef) + ) + if ti.public_content: + public_content.extend(wrap_with_ifdef(ti.public_content, field_ifdef)) # Only collect encode logic if this message needs it if needs_encode: - encode.append(ti.encode_content) - size_calc.append(ti.get_size_calculation(f"this->{ti.field_name}")) + # Check for field_ifdef option + field_ifdef = None + if field.options.HasExtension(pb.field_ifdef): + field_ifdef = field.options.Extensions[pb.field_ifdef] + + encode.extend(wrap_with_ifdef(ti.encode_content, field_ifdef)) + size_calc.extend( + wrap_with_ifdef( + ti.get_size_calculation(f"this->{ti.field_name}"), field_ifdef + ) + ) # Only collect decode methods if this message needs them if needs_decode: + # Check for field_ifdef option for decode as well + field_ifdef = None + if field.options.HasExtension(pb.field_ifdef): + field_ifdef = field.options.Extensions[pb.field_ifdef] + if ti.decode_varint_content: - decode_varint.append(ti.decode_varint_content) + decode_varint.extend( + wrap_with_ifdef(ti.decode_varint_content, field_ifdef) + ) if ti.decode_length_content: - decode_length.append(ti.decode_length_content) + decode_length.extend( + wrap_with_ifdef(ti.decode_length_content, field_ifdef) + ) if ti.decode_32bit_content: - decode_32bit.append(ti.decode_32bit_content) + decode_32bit.extend( + wrap_with_ifdef(ti.decode_32bit_content, field_ifdef) + ) if ti.decode_64bit_content: - decode_64bit.append(ti.decode_64bit_content) + decode_64bit.extend( + wrap_with_ifdef(ti.decode_64bit_content, field_ifdef) + ) if ti.dump_content: - dump.append(ti.dump_content) + # Check for field_ifdef option for dump as well + field_ifdef = None + if field.options.HasExtension(pb.field_ifdef): + field_ifdef = field.options.Extensions[pb.field_ifdef] + + dump.extend(wrap_with_ifdef(ti.dump_content, field_ifdef)) cpp = "" if decode_varint: - decode_varint.append("default:\n return false;") o = f"bool {desc.name}::decode_varint(uint32_t field_id, ProtoVarInt value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_varint), " ") + "\n" + o += " default: return false;\n" o += " }\n" + o += " return true;\n" o += "}\n" cpp += o prot = "bool decode_varint(uint32_t field_id, ProtoVarInt value) override;" protected_content.insert(0, prot) if decode_length: - decode_length.append("default:\n return false;") o = f"bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_length), " ") + "\n" + o += " default: return false;\n" o += " }\n" + o += " return true;\n" o += "}\n" cpp += o prot = "bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;" protected_content.insert(0, prot) if decode_32bit: - decode_32bit.append("default:\n return false;") o = f"bool {desc.name}::decode_32bit(uint32_t field_id, Proto32Bit value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_32bit), " ") + "\n" + o += " default: return false;\n" o += " }\n" + o += " return true;\n" o += "}\n" cpp += o prot = "bool decode_32bit(uint32_t field_id, Proto32Bit value) override;" protected_content.insert(0, prot) if decode_64bit: - decode_64bit.append("default:\n return false;") o = f"bool {desc.name}::decode_64bit(uint32_t field_id, Proto64Bit value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_64bit), " ") + "\n" + o += " default: return false;\n" o += " }\n" + o += " return true;\n" o += "}\n" cpp += o prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;" @@ -1225,7 +1380,9 @@ def build_message_type( if base_class: out = f"class {desc.name} : public {base_class} {{\n" else: - out = f"class {desc.name} : public ProtoMessage {{\n" + # Determine inheritance based on whether the message needs decoding + base_class = "ProtoDecodableMessage" if needs_decode else "ProtoMessage" + out = f"class {desc.name} : public {base_class} {{\n" out += " public:\n" out += indent("\n".join(public_content)) + "\n" out += "\n" @@ -1261,6 +1418,17 @@ def get_opt( return desc.options.Extensions[opt] +def get_field_opt( + field: descriptor.FieldDescriptorProto, + opt: descriptor.FieldOptions, + default: Any = None, +) -> Any: + """Get the option from a field descriptor.""" + if not field.options.HasExtension(opt): + return default + return field.options.Extensions[opt] + + def get_base_class(desc: descriptor.DescriptorProto) -> str | None: """Get the base_class option from a message descriptor.""" if not desc.options.HasExtension(pb.base_class): @@ -1326,6 +1494,8 @@ def find_common_fields( def build_base_class( base_class_name: str, common_fields: list[descriptor.FieldDescriptorProto], + messages: list[descriptor.DescriptorProto], + message_source_map: dict[str, int], ) -> tuple[str, str, str]: """Build the base class definition and implementation.""" public_content = [] @@ -1334,17 +1504,21 @@ def build_base_class( # For base classes, we only declare the fields but don't handle encode/decode # The derived classes will handle encoding/decoding with their specific field numbers for field in common_fields: - if field.label == 3: # repeated - ti = RepeatedTypeInfo(field) - else: - ti = TYPE_INFO[field.type](field) + ti = create_field_type_info(field) # Only add field declarations, not encode/decode logic protected_content.extend(ti.protected_content) public_content.extend(ti.public_content) + # Determine if any message using this base class needs decoding + needs_decode = any( + message_source_map.get(msg.name, SOURCE_BOTH) in (SOURCE_BOTH, SOURCE_CLIENT) + for msg in messages + ) + # Build header - out = f"class {base_class_name} : public ProtoMessage {{\n" + parent_class = "ProtoDecodableMessage" if needs_decode else "ProtoMessage" + out = f"class {base_class_name} : public {parent_class} {{\n" out += " public:\n" # Add destructor with override @@ -1370,6 +1544,7 @@ def build_base_class( def generate_base_classes( base_class_groups: dict[str, list[descriptor.DescriptorProto]], + message_source_map: dict[str, int], ) -> tuple[str, str, str]: """Generate all base classes.""" all_headers = [] @@ -1382,7 +1557,9 @@ def generate_base_classes( if common_fields: # Generate base class - header, cpp, dump_cpp = build_base_class(base_class_name, common_fields) + header, cpp, dump_cpp = build_base_class( + base_class_name, common_fields, messages, message_source_map + ) all_headers.append(header) all_cpp.append(cpp) all_dump_cpp.append(dump_cpp) @@ -1392,6 +1569,7 @@ def generate_base_classes( def build_service_message_type( mt: descriptor.DescriptorProto, + message_source_map: dict[str, int], ) -> tuple[str, str] | None: """Builds the service message type.""" snake = camel_to_snake(mt.name) @@ -1399,7 +1577,7 @@ def build_service_message_type( if id_ is None: return None - source: int = get_opt(mt, pb.source, 0) + source: int = message_source_map.get(mt.name, SOURCE_BOTH) ifdef: str | None = get_opt(mt, pb.ifdef) log: bool = get_opt(mt, pb.log, True) @@ -1470,6 +1648,7 @@ namespace api { #include "api_pb2.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" + #include namespace esphome { namespace api { @@ -1494,7 +1673,7 @@ namespace api { content += "namespace enums {\n\n" # Build dynamic ifdef mappings for both enums and messages - enum_ifdef_map, message_ifdef_map = build_type_usage_map(file) + enum_ifdef_map, message_ifdef_map, message_source_map = build_type_usage_map(file) # Simple grouping of enums by ifdef current_ifdef = None @@ -1538,7 +1717,9 @@ namespace api { # Generate base classes if base_class_fields: - base_headers, base_cpp, base_dump_cpp = generate_base_classes(base_class_groups) + base_headers, base_cpp, base_dump_cpp = generate_base_classes( + base_class_groups, message_source_map + ) content += base_headers cpp += base_cpp dump_cpp += base_dump_cpp @@ -1548,7 +1729,7 @@ namespace api { current_ifdef = None for m in mt: - s, c, dc = build_message_type(m, base_class_fields) + s, c, dc = build_message_type(m, base_class_fields, message_source_map) msg_ifdef = message_ifdef_map.get(m.name) # Handle ifdef changes @@ -1640,13 +1821,12 @@ static const char *const TAG = "api.service"; hpp += " public:\n" hpp += "#endif\n\n" - # Add generic send_message method - hpp += " template\n" - hpp += " bool send_message(const T &msg) {\n" + # Add non-template send_message method + hpp += " bool send_message(const ProtoMessage &msg, uint8_t message_type) {\n" hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" hpp += " this->log_send_message_(msg.message_name(), msg.dump());\n" hpp += "#endif\n" - hpp += " return this->send_message_(msg, T::MESSAGE_TYPE);\n" + hpp += " return this->send_message_(msg, message_type);\n" hpp += " }\n\n" # Add logging helper method implementation to cpp @@ -1657,7 +1837,7 @@ static const char *const TAG = "api.service"; cpp += "#endif\n\n" for mt in file.message_type: - obj = build_service_message_type(mt) + obj = build_service_message_type(mt, message_source_map) if obj is None: continue hout, cout = obj @@ -1732,7 +1912,9 @@ static const char *const TAG = "api.service"; handler_body = f"this->{func}(msg);\n" else: handler_body = f"{ret} ret = this->{func}(msg);\n" - handler_body += "if (!this->send_message(ret)) {\n" + handler_body += ( + f"if (!this->send_message(ret, {ret}::MESSAGE_TYPE)) {{\n" + ) handler_body += " this->on_fatal_error();\n" handler_body += "}\n" @@ -1745,7 +1927,7 @@ static const char *const TAG = "api.service"; body += f"this->{func}(msg);\n" else: body += f"{ret} ret = this->{func}(msg);\n" - body += "if (!this->send_message(ret)) {\n" + body += f"if (!this->send_message(ret, {ret}::MESSAGE_TYPE)) {{\n" body += " this->on_fatal_error();\n" body += "}\n" diff --git a/script/ci-custom.py b/script/ci-custom.py index 1310a93230..1172c7152f 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -241,6 +241,9 @@ def lint_ext_check(fname): "docker/ha-addon-rootfs/**", "docker/*.py", "script/*", + "CLAUDE.md", + "GEMINI.md", + ".github/copilot-instructions.md", ] ) def lint_executable_bit(fname): diff --git a/script/helpers.py b/script/helpers.py index ff63bbc5b6..9032451b4f 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -530,27 +530,26 @@ def get_components_from_integration_fixtures() -> set[str]: Returns: Set of component names used in integration test fixtures """ - import yaml + from esphome import yaml_util components: set[str] = set() fixtures_dir = Path(__file__).parent.parent / "tests" / "integration" / "fixtures" for yaml_file in fixtures_dir.glob("*.yaml"): - with open(yaml_file) as f: - config: dict[str, any] | None = yaml.safe_load(f) - if not config: + config: dict[str, any] | None = yaml_util.load_yaml(str(yaml_file)) + if not config: + continue + + # Add all top-level component keys + components.update(config.keys()) + + # Add platform components (e.g., output.template) + for value in config.values(): + if not isinstance(value, list): continue - # Add all top-level component keys - components.update(config.keys()) - - # Add platform components (e.g., output.template) - for value in config.values(): - if not isinstance(value, list): - continue - - for item in value: - if isinstance(item, dict) and "platform" in item: - components.add(item["platform"]) + for item in value: + if isinstance(item, dict) and "platform" in item: + components.add(item["platform"]) return components diff --git a/script/lint-python b/script/lint-python index 2c25e4aee0..18281c711e 100755 --- a/script/lint-python +++ b/script/lint-python @@ -137,7 +137,7 @@ def main(): print() print("Running pyupgrade...") print() - PYUPGRADE_TARGET = "--py310-plus" + PYUPGRADE_TARGET = "--py311-plus" for files in filesets: cmd = ["pyupgrade", PYUPGRADE_TARGET] + files log = get_err(*cmd) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..d49aac4bab --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""ESPHome tests package.""" diff --git a/tests/component_tests/__init__.py b/tests/component_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/binary_sensor/__init__.py b/tests/component_tests/binary_sensor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/button/__init__.py b/tests/component_tests/button/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py index b1e0eaa200..b269e23cd6 100644 --- a/tests/component_tests/conftest.py +++ b/tests/component_tests/conftest.py @@ -5,18 +5,30 @@ from __future__ import annotations from collections.abc import Callable, Generator from pathlib import Path import sys +from typing import Any import pytest +from esphome import config, final_validate +from esphome.const import ( + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PlatformFramework, +) +from esphome.types import ConfigType + # Add package root to python path here = Path(__file__).parent package_root = here.parent.parent sys.path.insert(0, package_root.as_posix()) from esphome.__main__ import generate_cpp_contents # noqa: E402 -from esphome.config import read_config # noqa: E402 +from esphome.config import Config, read_config # noqa: E402 from esphome.core import CORE # noqa: E402 +from .types import SetCoreConfigCallable # noqa: E402 + @pytest.fixture(autouse=True) def config_path(request: pytest.FixtureRequest) -> Generator[None]: @@ -36,6 +48,59 @@ def config_path(request: pytest.FixtureRequest) -> Generator[None]: CORE.config_path = original_path +@pytest.fixture(autouse=True) +def reset_core() -> Generator[None]: + """Reset CORE after each test.""" + yield + CORE.reset() + + +@pytest.fixture +def set_core_config() -> Generator[SetCoreConfigCallable]: + """Fixture to set up the core configuration for tests.""" + + def setter( + platform_framework: PlatformFramework, + /, + *, + core_data: ConfigType | None = None, + platform_data: ConfigType | None = None, + ) -> None: + platform, framework = platform_framework.value + + # Set base core configuration + CORE.data[KEY_CORE] = { + KEY_TARGET_PLATFORM: platform.value, + KEY_TARGET_FRAMEWORK: framework.value, + } + + # Update with any additional core data + if core_data: + CORE.data[KEY_CORE].update(core_data) + + # Set platform-specific data + if platform_data: + CORE.data[platform.value] = platform_data + + config.path_context.set([]) + final_validate.full_config.set(Config()) + + yield setter + + +@pytest.fixture +def set_component_config() -> Callable[[str, Any], None]: + """ + Fixture to set a component configuration in the mock config. + This must be used after the core configuration has been set up. + """ + + def setter(name: str, value: Any) -> None: + final_validate.full_config.get()[name] = value + + return setter + + @pytest.fixture def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]: """Return a function to get absolute paths relative to the component's fixtures directory.""" @@ -60,7 +125,7 @@ def component_config_path(request: pytest.FixtureRequest) -> Callable[[str], Pat @pytest.fixture def generate_main() -> Generator[Callable[[str | Path], str]]: - """Generates the C++ main.cpp file and returns it in string form.""" + """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) @@ -69,5 +134,3 @@ def generate_main() -> Generator[Callable[[str | Path], str]]: return CORE.cpp_main_section yield generator - - CORE.reset() diff --git a/tests/component_tests/deep_sleep/__init__.py b/tests/component_tests/deep_sleep/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py new file mode 100644 index 0000000000..fe031c653f --- /dev/null +++ b/tests/component_tests/esp32/test_esp32.py @@ -0,0 +1,73 @@ +""" +Test ESP32 configuration +""" + +from typing import Any + +import pytest + +from esphome.components.esp32 import VARIANTS +import esphome.config_validation as cv +from esphome.const import PlatformFramework + + +def test_esp32_config(set_core_config) -> None: + set_core_config(PlatformFramework.ESP32_IDF) + + from esphome.components.esp32 import CONFIG_SCHEMA + from esphome.components.esp32.const import VARIANT_ESP32, VARIANT_FRIENDLY + + # Example ESP32 configuration + config = { + "board": "esp32dev", + "variant": VARIANT_ESP32, + "cpu_frequency": "240MHz", + "flash_size": "4MB", + "framework": { + "type": "esp-idf", + }, + } + + # Check if the variant is valid + config = CONFIG_SCHEMA(config) + assert config["variant"] == VARIANT_ESP32 + + # Check that defining a variant sets the board name correctly + for variant in VARIANTS: + config = CONFIG_SCHEMA( + { + "variant": variant, + } + ) + assert VARIANT_FRIENDLY[variant].lower() in config["board"] + + +@pytest.mark.parametrize( + ("config", "error_match"), + [ + pytest.param( + {"flash_size": "4MB"}, + r"This board is unknown, if you are sure you want to compile with this board selection, override with option 'variant' @ data\['board'\]", + id="unknown_board_config", + ), + pytest.param( + {"variant": "esp32xx"}, + r"Unknown value 'ESP32XX', did you mean 'ESP32', 'ESP32S3', 'ESP32S2'\? for dictionary value @ data\['variant'\]", + id="unknown_variant_config", + ), + pytest.param( + {"variant": "esp32s3", "board": "esp32dev"}, + r"Option 'variant' does not match selected board. @ data\['variant'\]", + id="mismatched_board_variant_config", + ), + ], +) +def test_esp32_configuration_errors( + config: Any, + error_match: str, +) -> None: + """Test detection of invalid configuration.""" + from esphome.components.esp32 import CONFIG_SCHEMA + + with pytest.raises(cv.Invalid, match=error_match): + CONFIG_SCHEMA(config) diff --git a/tests/component_tests/image/__init__.py b/tests/component_tests/image/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/mipi_spi/__init__.py b/tests/component_tests/mipi_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/mipi_spi/fixtures/lvgl.yaml b/tests/component_tests/mipi_spi/fixtures/lvgl.yaml new file mode 100644 index 0000000000..bc800e1090 --- /dev/null +++ b/tests/component_tests/mipi_spi/fixtures/lvgl.yaml @@ -0,0 +1,25 @@ +esphome: + name: c3-7735 + +esp32: + board: lolin_c3_mini + +spi: + mosi_pin: + number: GPIO2 + ignore_strapping_warning: true + clk_pin: GPIO1 + +display: + - platform: mipi_spi + data_rate: 20MHz + model: st7735 + cs_pin: + number: GPIO8 + ignore_strapping_warning: true + dc_pin: + number: GPIO3 + reset_pin: + number: GPIO4 + +lvgl: diff --git a/tests/component_tests/mipi_spi/fixtures/native.yaml b/tests/component_tests/mipi_spi/fixtures/native.yaml new file mode 100644 index 0000000000..6962ac25c7 --- /dev/null +++ b/tests/component_tests/mipi_spi/fixtures/native.yaml @@ -0,0 +1,20 @@ +esphome: + name: jc3636w518 + +esp32: + board: esp32-s3-devkitc-1 + framework: + type: esp-idf + +psram: + mode: octal + +spi: + id: display_qspi + type: quad + clk_pin: 9 + data_pins: [11, 12, 13, 14] + +display: + - platform: mipi_spi + model: jc3636w518 diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py new file mode 100644 index 0000000000..96abad02ad --- /dev/null +++ b/tests/component_tests/mipi_spi/test_init.py @@ -0,0 +1,387 @@ +"""Tests for mpip_spi configuration validation.""" + +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.esp32 import ( + KEY_BOARD, + KEY_ESP32, + KEY_VARIANT, + VARIANT_ESP32, + VARIANT_ESP32S3, + VARIANTS, +) +from esphome.components.esp32.gpio import validate_gpio_pin +from esphome.components.mipi_spi.display import ( + CONF_BUS_MODE, + CONF_NATIVE_HEIGHT, + CONFIG_SCHEMA, + FINAL_VALIDATE_SCHEMA, + MODELS, + dimension_schema, +) +from esphome.const import ( + CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_HEIGHT, + CONF_INIT_SEQUENCE, + CONF_WIDTH, + PlatformFramework, +) +from esphome.core import CORE +from esphome.pins import internal_gpio_pin_number +from esphome.types import ConfigType +from tests.component_tests.types import SetCoreConfigCallable + + +def run_schema_validation(config: ConfigType) -> None: + """Run schema validation on a configuration.""" + FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) + + +@pytest.fixture +def choose_variant_with_pins() -> Callable[..., None]: + """ + Set the ESP32 variant for the given model based on pins. For ESP32 only since the other platforms + do not have variants. + """ + + def chooser(*pins: int | str | None) -> None: + for v in VARIANTS: + try: + CORE.data[KEY_ESP32][KEY_VARIANT] = v + for pin in pins: + if pin is not None: + pin = internal_gpio_pin_number(pin) + validate_gpio_pin(pin) + return + except cv.Invalid: + continue + + return chooser + + +@pytest.mark.parametrize( + ("config", "error_match"), + [ + pytest.param( + "a string", + "expected a dictionary", + id="invalid_string_config", + ), + pytest.param( + {"id": "display_id"}, + r"required key not provided @ data\['model'\]", + id="missing_model", + ), + pytest.param( + {"id": "display_id", "model": "custom", "init_sequence": [[0x36, 0x01]]}, + r"required key not provided @ data\['dimensions'\]", + id="missing_dimensions", + ), + pytest.param( + { + "model": "custom", + "dc_pin": 18, + "dimensions": {"width": 320, "height": 240}, + }, + r"required key not provided @ data\['init_sequence'\]", + id="missing_init_sequence", + ), + pytest.param( + { + "id": "display_id", + "model": "custom", + "dimensions": {"width": 320, "height": 240}, + "draw_rounding": 13, + "init_sequence": [[0xA0, 0x01]], + }, + r"value must be a power of two for dictionary value @ data\['draw_rounding'\]", + id="invalid_draw_rounding", + ), + ], +) +def test_basic_configuration_errors( + config: str | ConfigType, + error_match: str, + set_core_config: SetCoreConfigCallable, +) -> None: + """Test basic configuration validation errors""" + + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + with pytest.raises(cv.Invalid, match=error_match): + run_schema_validation(config) + + +@pytest.mark.parametrize( + ("rounding", "config", "error_match"), + [ + pytest.param( + 4, + {"width": 320}, + r"required key not provided @ data\['height'\]", + id="missing_height", + ), + pytest.param( + 32, + {"width": 320, "height": 111}, + "Dimensions and offsets must be divisible by 32", + id="dimensions_not_divisible", + ), + ], +) +def test_dimension_validation( + rounding: int, + config: ConfigType, + error_match: str, + set_core_config: SetCoreConfigCallable, +) -> None: + """Test dimension-related validation errors""" + + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + with pytest.raises(cv.Invalid, match=error_match): + dimension_schema(rounding)(config) + + +@pytest.mark.parametrize( + ("config", "error_match"), + [ + pytest.param( + { + "model": "JC3248W535", + "transform": {"mirror_x": False, "mirror_y": True, "swap_xy": True}, + }, + "Axis swapping not supported by this model", + id="axis_swapping_not_supported", + ), + pytest.param( + { + "model": "custom", + "dimensions": {"width": 320, "height": 240}, + "transform": {"mirror_x": False, "mirror_y": True, "swap_xy": False}, + "init_sequence": [[0x36, 0x01]], + }, + r"transform is not supported when MADCTL \(0X36\) is in the init sequence", + id="transform_with_madctl", + ), + pytest.param( + { + "model": "custom", + "dimensions": {"width": 320, "height": 240}, + "init_sequence": [[0x3A, 0x01]], + }, + r"PIXFMT \(0X3A\) should not be in the init sequence, it will be set automatically", + id="pixfmt_in_init_sequence", + ), + ], +) +def test_transform_and_init_sequence_errors( + config: ConfigType, + error_match: str, + set_core_config: SetCoreConfigCallable, +) -> None: + """Test transform and init sequence validation errors""" + + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + with pytest.raises(cv.Invalid, match=error_match): + run_schema_validation(config) + + +@pytest.mark.parametrize( + ("config", "error_match"), + [ + pytest.param( + {"model": "t4-s3", "dc_pin": 18}, + "DC pin is not supported in quad mode", + id="dc_pin_not_supported_quad_mode", + ), + pytest.param( + {"model": "t4-s3", "color_depth": 18}, + "Unknown value '18', valid options are '16', '16bit", + id="invalid_color_depth_t4_s3", + ), + pytest.param( + {"model": "t-embed", "color_depth": 24}, + "Unknown value '24', valid options are '16', '8", + id="invalid_color_depth_t_embed", + ), + pytest.param( + {"model": "ili9488"}, + "DC pin is required in single mode", + id="dc_pin_required_single_mode", + ), + pytest.param( + {"model": "wt32-sc01-plus", "brightness": 128}, + r"extra keys not allowed @ data\['brightness'\]", + id="brightness_not_supported", + ), + pytest.param( + {"model": "T-DISPLAY-S3-PRO"}, + "PSRAM is required for this display", + id="psram_required", + ), + ], +) +def test_esp32s3_specific_errors( + config: ConfigType, + error_match: str, + set_core_config: SetCoreConfigCallable, +) -> None: + """Test ESP32-S3 specific configuration errors""" + + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + ) + + with pytest.raises(cv.Invalid, match=error_match): + run_schema_validation(config) + + +def test_framework_specific_errors( + set_core_config: SetCoreConfigCallable, +) -> None: + """Test framework-specific configuration errors""" + + set_core_config( + PlatformFramework.ESP32_ARDUINO, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + with pytest.raises( + cv.Invalid, + match=r"This feature is only available with frameworks \['esp-idf'\]", + ): + run_schema_validation({"model": "wt32-sc01-plus"}) + + +def test_custom_model_with_all_options( + set_core_config: SetCoreConfigCallable, +) -> None: + """Test custom model configuration with all available options.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + ) + + run_schema_validation( + { + "model": "custom", + "pixel_mode": "18bit", + "color_depth": 8, + "id": "display_id", + "byte_order": "little_endian", + "bus_mode": "single", + "color_order": "rgb", + "dc_pin": 11, + "reset_pin": 12, + "enable_pin": 13, + "cs_pin": 14, + "init_sequence": [[0xA0, 0x01]], + "dimensions": { + "width": 320, + "height": 240, + "offset_width": 32, + "offset_height": 32, + }, + "invert_colors": True, + "transform": {"mirror_x": True, "mirror_y": True, "swap_xy": False}, + "spi_mode": "mode0", + "data_rate": "40MHz", + "use_axis_flips": True, + "draw_rounding": 4, + "spi_16": True, + "buffer_size": 0.25, + } + ) + + +def test_all_predefined_models( + set_core_config: SetCoreConfigCallable, + set_component_config: Callable[[str, Any], None], + choose_variant_with_pins: Callable[..., None], +) -> None: + """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}, + ) + + # Enable PSRAM which is required for some models + set_component_config("psram", True) + + # Test all models, providing default values where necessary + for name, model in MODELS.items(): + config = {"model": name} + + # Get the pins required by this model and find a compatible variant + pins = [ + pin + for pin in [ + model.get_default(pin, None) + for pin in ("dc_pin", "reset_pin", "cs_pin") + ] + if pin is not None + ] + choose_variant_with_pins(pins) + + # Add required fields that don't have defaults + if ( + not model.get_default(CONF_DC_PIN) + and model.get_default(CONF_BUS_MODE) != "quad" + ): + config[CONF_DC_PIN] = 14 + if not model.get_default(CONF_NATIVE_HEIGHT): + config[CONF_DIMENSIONS] = {CONF_HEIGHT: 240, CONF_WIDTH: 320} + if model.initsequence is None: + config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]] + + run_schema_validation(config) + + +def test_native_generation( + generate_main: Callable[[str | Path], str], + component_fixture_path: Callable[[str], Path], +) -> None: + """Test code generation for display.""" + + main_cpp = generate_main(component_fixture_path("native.yaml")) + assert ( + "mipi_spi::MipiSpiBuffer()" + in main_cpp + ) + assert "set_init_sequence({240, 1, 8, 242" in main_cpp + assert "show_test_card();" in main_cpp + assert "set_write_only(true);" in main_cpp + + +def test_lvgl_generation( + generate_main: Callable[[str | Path], str], + component_fixture_path: Callable[[str], Path], +) -> None: + """Test LVGL generation configuration.""" + + main_cpp = generate_main(component_fixture_path("lvgl.yaml")) + assert ( + "mipi_spi::MipiSpi();" + in main_cpp + ) + assert "set_init_sequence({1, 0, 10, 255, 177" in main_cpp + assert "show_test_card();" not in main_cpp + assert "set_auto_clear(false);" in main_cpp diff --git a/tests/component_tests/ota/__init__.py b/tests/component_tests/ota/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/packages/__init__.py b/tests/component_tests/packages/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/sensor/__init__.py b/tests/component_tests/sensor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/text/__init__.py b/tests/component_tests/text/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/text_sensor/__init__.py b/tests/component_tests/text_sensor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/types.py b/tests/component_tests/types.py new file mode 100644 index 0000000000..72b8be4503 --- /dev/null +++ b/tests/component_tests/types.py @@ -0,0 +1,21 @@ +"""Type definitions for component tests.""" + +from __future__ import annotations + +from typing import Protocol + +from esphome.const import PlatformFramework +from esphome.types import ConfigType + + +class SetCoreConfigCallable(Protocol): + """Protocol for the set_core_config fixture setter function.""" + + def __call__( # noqa: E704 + self, + platform_framework: PlatformFramework, + /, + *, + core_data: ConfigType | None = None, + platform_data: ConfigType | None = None, + ) -> None: ... diff --git a/tests/component_tests/web_server/__init__.py b/tests/component_tests/web_server/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/web_server/test_ota_migration.py b/tests/component_tests/web_server/test_ota_migration.py index 7f34ec75f6..da25bab0e8 100644 --- a/tests/component_tests/web_server/test_ota_migration.py +++ b/tests/component_tests/web_server/test_ota_migration.py @@ -8,31 +8,31 @@ from esphome.types import ConfigType def test_web_server_ota_true_fails_validation() -> None: """Test that web_server with ota: true fails validation with helpful message.""" - from esphome.components.web_server import validate_ota_removed + from esphome.components.web_server import validate_ota # Config with ota: true should fail config: ConfigType = {"ota": True} with pytest.raises(cv.Invalid) as exc_info: - validate_ota_removed(config) + validate_ota(config) # Check error message contains migration instructions error_msg = str(exc_info.value) - assert "has been removed from 'web_server'" in error_msg + assert "only accepts 'false' to disable OTA" in error_msg assert "platform: web_server" in error_msg assert "ota:" in error_msg def test_web_server_ota_false_passes_validation() -> None: """Test that web_server with ota: false passes validation.""" - from esphome.components.web_server import validate_ota_removed + from esphome.components.web_server import validate_ota # Config with ota: false should pass config: ConfigType = {"ota": False} - result = validate_ota_removed(config) + result = validate_ota(config) assert result == config # Config without ota should also pass config: ConfigType = {} - result = validate_ota_removed(config) + result = validate_ota(config) assert result == config diff --git a/tests/components/adc/common.yaml b/tests/components/adc/common.yaml new file mode 100644 index 0000000000..ebdd1aece5 --- /dev/null +++ b/tests/components/adc/common.yaml @@ -0,0 +1,11 @@ +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 491d0af3b9..0a3d5d1fdc 100644 --- a/tests/components/adc/test.bk72xx-ard.yaml +++ b/tests/components/adc/test.bk72xx-ard.yaml @@ -1,4 +1,7 @@ +packages: + base: !include common.yaml + sensor: - - platform: adc + - id: !extend my_sensor pin: P23 - name: Basic ADC Test + attenuation: !remove diff --git a/tests/components/adc/test.esp32-ard.yaml b/tests/components/adc/test.esp32-ard.yaml index 923fd0d706..e6a1fd3bd9 100644 --- a/tests/components/adc/test.esp32-ard.yaml +++ b/tests/components/adc/test.esp32-ard.yaml @@ -1,11 +1,6 @@ +packages: + base: !include common.yaml + sensor: - - platform: adc + - id: !extend my_sensor pin: A0 - name: Living Room Brightness - 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-c3-ard.yaml b/tests/components/adc/test.esp32-c3-ard.yaml index e74477c582..ea3b00a85f 100644 --- a/tests/components/adc/test.esp32-c3-ard.yaml +++ b/tests/components/adc/test.esp32-c3-ard.yaml @@ -1,5 +1,6 @@ +packages: + base: !include common.yaml + sensor: - - platform: adc - id: my_sensor + - id: !extend my_sensor pin: 4 - attenuation: 12db diff --git a/tests/components/adc/test.esp32-c3-idf.yaml b/tests/components/adc/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ea3b00a85f --- /dev/null +++ b/tests/components/adc/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +packages: + base: !include common.yaml + +sensor: + - id: !extend my_sensor + pin: 4 diff --git a/tests/components/adc/test.esp32-idf.yaml b/tests/components/adc/test.esp32-idf.yaml index 923fd0d706..e6a1fd3bd9 100644 --- a/tests/components/adc/test.esp32-idf.yaml +++ b/tests/components/adc/test.esp32-idf.yaml @@ -1,11 +1,6 @@ +packages: + base: !include common.yaml + sensor: - - platform: adc + - id: !extend my_sensor pin: A0 - name: Living Room Brightness - 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 index e1a6bc22e5..bbd91c5e5a 100644 --- a/tests/components/adc/test.esp32-s2-ard.yaml +++ b/tests/components/adc/test.esp32-s2-ard.yaml @@ -1,5 +1,6 @@ +packages: + base: !include common.yaml + sensor: - - platform: adc - id: my_sensor + - id: !extend my_sensor pin: 1 - attenuation: 12db diff --git a/tests/components/adc/test.esp32-s2-idf.yaml b/tests/components/adc/test.esp32-s2-idf.yaml new file mode 100644 index 0000000000..bbd91c5e5a --- /dev/null +++ b/tests/components/adc/test.esp32-s2-idf.yaml @@ -0,0 +1,6 @@ +packages: + base: !include common.yaml + +sensor: + - id: !extend my_sensor + pin: 1 diff --git a/tests/components/adc/test.esp32-s3-ard.yaml b/tests/components/adc/test.esp32-s3-ard.yaml index e1a6bc22e5..bbd91c5e5a 100644 --- a/tests/components/adc/test.esp32-s3-ard.yaml +++ b/tests/components/adc/test.esp32-s3-ard.yaml @@ -1,5 +1,6 @@ +packages: + base: !include common.yaml + sensor: - - platform: adc - id: my_sensor + - id: !extend my_sensor pin: 1 - attenuation: 12db diff --git a/tests/components/adc/test.esp32-s3-idf.yaml b/tests/components/adc/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..bbd91c5e5a --- /dev/null +++ b/tests/components/adc/test.esp32-s3-idf.yaml @@ -0,0 +1,6 @@ +packages: + base: !include common.yaml + +sensor: + - id: !extend my_sensor + pin: 1 diff --git a/tests/components/adc/test.esp8266-ard.yaml b/tests/components/adc/test.esp8266-ard.yaml index 1ef79c7ca1..bcb3620cfc 100644 --- a/tests/components/adc/test.esp8266-ard.yaml +++ b/tests/components/adc/test.esp8266-ard.yaml @@ -1,4 +1,7 @@ +packages: + base: !include common.yaml + sensor: - - platform: adc - id: my_sensor + - id: !extend my_sensor pin: VCC + attenuation: !remove diff --git a/tests/components/adc/test.ln882x-ard.yaml b/tests/components/adc/test.ln882x-ard.yaml index 92c76ca9b3..0622cd7b27 100644 --- a/tests/components/adc/test.ln882x-ard.yaml +++ b/tests/components/adc/test.ln882x-ard.yaml @@ -1,4 +1,7 @@ +packages: + base: !include common.yaml + sensor: - - platform: adc + - id: !extend my_sensor pin: PA0 - name: Basic ADC Test + attenuation: !remove diff --git a/tests/components/adc/test.rp2040-ard.yaml b/tests/components/adc/test.rp2040-ard.yaml index 200b802a4d..bcb3620cfc 100644 --- a/tests/components/adc/test.rp2040-ard.yaml +++ b/tests/components/adc/test.rp2040-ard.yaml @@ -1,4 +1,7 @@ +packages: + base: !include common.yaml + sensor: - - platform: adc + - id: !extend my_sensor pin: VCC - name: VSYS + attenuation: !remove diff --git a/tests/components/esp32_camera/common.yaml b/tests/components/esp32_camera/common.yaml index 2f5f792f1c..64f75c699a 100644 --- a/tests/components/esp32_camera/common.yaml +++ b/tests/components/esp32_camera/common.yaml @@ -22,6 +22,7 @@ esp32_camera: power_down_pin: 1 resolution: 640x480 jpeg_quality: 10 + frame_buffer_location: PSRAM on_image: then: - lambda: |- diff --git a/tests/components/gpio/test.nrf52-adafruit.yaml b/tests/components/gpio/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..3ca285117d --- /dev/null +++ b/tests/components/gpio/test.nrf52-adafruit.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: gpio + pin: 2 + id: gpio_binary_sensor + +output: + - platform: gpio + pin: 3 + id: gpio_output + +switch: + - platform: gpio + pin: 4 + id: gpio_switch diff --git a/tests/components/gpio/test.nrf52-mcumgr.yaml b/tests/components/gpio/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..3ca285117d --- /dev/null +++ b/tests/components/gpio/test.nrf52-mcumgr.yaml @@ -0,0 +1,14 @@ +binary_sensor: + - platform: gpio + pin: 2 + id: gpio_binary_sensor + +output: + - platform: gpio + pin: 3 + id: gpio_output + +switch: + - platform: gpio + pin: 4 + id: gpio_switch diff --git a/tests/components/logger/test-on_message.host.yaml b/tests/components/logger/test-on_message.host.yaml new file mode 100644 index 0000000000..12211a257b --- /dev/null +++ b/tests/components/logger/test-on_message.host.yaml @@ -0,0 +1,18 @@ +logger: + id: logger_id + level: DEBUG + on_message: + - level: DEBUG + then: + - lambda: |- + ESP_LOGD("test", "Got message level %d: %s - %s", level, tag, message); + - level: WARN + then: + - lambda: |- + ESP_LOGW("test", "Warning level %d from %s", level, tag); + - level: ERROR + then: + - lambda: |- + // Test that level is uint8_t by using it in calculations + uint8_t adjusted_level = level + 1; + ESP_LOGE("test", "Error with adjusted level %d", adjusted_level); diff --git a/tests/components/logger/test.nrf52-adafruit.yaml b/tests/components/logger/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..70b485daac --- /dev/null +++ b/tests/components/logger/test.nrf52-adafruit.yaml @@ -0,0 +1,7 @@ +esphome: + on_boot: + then: + - logger.log: Hello world + +logger: + level: DEBUG diff --git a/tests/components/logger/test.nrf52-mcumgr.yaml b/tests/components/logger/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..70b485daac --- /dev/null +++ b/tests/components/logger/test.nrf52-mcumgr.yaml @@ -0,0 +1,7 @@ +esphome: + on_boot: + then: + - logger.log: Hello world + +logger: + level: DEBUG diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index fbcd2a3fba..46341c266d 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -928,6 +928,12 @@ lvgl: angle_range: 360 rotation: !lambda return 2700; indicators: + - tick_style: + start_value: 0 + end_value: 60 + color_start: 0x0000bd + color_end: 0xbd0000 + width: !lambda return 1; - line: opa: 50% id: minute_hand diff --git a/tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml deleted file mode 100644 index a28776798c..0000000000 --- a/tests/components/mipi_spi/test-esp32-2432s028.esp32-s3-idf.yaml +++ /dev/null @@ -1,41 +0,0 @@ -spi: - - id: quad_spi - type: quad - interface: spi3 - clk_pin: - number: 47 - data_pins: - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: octal_spi - type: octal - interface: hardware - clk_pin: - number: 0 - data_pins: - - 36 - - 37 - - 38 - - 39 - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: spi_id_3 - interface: any - clk_pin: 8 - mosi_pin: 9 - -display: - - platform: mipi_spi - model: ESP32-2432S028 diff --git a/tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml deleted file mode 100644 index 02b8f78d58..0000000000 --- a/tests/components/mipi_spi/test-jc3248w535.esp32-s3-idf.yaml +++ /dev/null @@ -1,41 +0,0 @@ -spi: - - id: quad_spi - type: quad - interface: spi3 - clk_pin: - number: 47 - data_pins: - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: octal_spi - type: octal - interface: hardware - clk_pin: - number: 0 - data_pins: - - 36 - - 37 - - 38 - - 39 - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: spi_id_3 - interface: any - clk_pin: 8 - mosi_pin: 9 - -display: - - platform: mipi_spi - model: JC3248W535 diff --git a/tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml deleted file mode 100644 index 147d4833ac..0000000000 --- a/tests/components/mipi_spi/test-jc3636w518.esp32-s3-idf.yaml +++ /dev/null @@ -1,19 +0,0 @@ -spi: - - id: quad_spi - type: quad - interface: spi3 - clk_pin: - number: 36 - data_pins: - - number: 40 - - number: 41 - - number: 42 - - number: 43 - - id: spi_id_3 - interface: any - clk_pin: 8 - mosi_pin: 9 - -display: - - platform: mipi_spi - model: JC3636W518 diff --git a/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml new file mode 100644 index 0000000000..e0f65a3a6a --- /dev/null +++ b/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml @@ -0,0 +1,18 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + +spi: + - id: spi_single + clk_pin: + number: ${clk_pin} + mosi_pin: + number: ${mosi_pin} + +display: + - platform: mipi_spi + model: t-display-s3-pro + +lvgl: + +psram: diff --git a/tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml deleted file mode 100644 index 8d96f31fd5..0000000000 --- a/tests/components/mipi_spi/test-pico-restouch-lcd-35.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -spi: - - id: spi_id_3 - interface: any - clk_pin: 8 - mosi_pin: 9 - -display: - - platform: mipi_spi - model: Pico-ResTouch-LCD-3.5 diff --git a/tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml deleted file mode 100644 index 98f6955bf3..0000000000 --- a/tests/components/mipi_spi/test-s3box.esp32-s3-idf.yaml +++ /dev/null @@ -1,41 +0,0 @@ -spi: - - id: quad_spi - type: quad - interface: spi3 - clk_pin: - number: 47 - data_pins: - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: octal_spi - type: octal - interface: hardware - clk_pin: - number: 0 - data_pins: - - 36 - - 37 - - 38 - - 39 - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: spi_id_3 - interface: any - clk_pin: 8 - mosi_pin: 9 - -display: - - platform: mipi_spi - model: S3BOX diff --git a/tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml deleted file mode 100644 index 11ad869d54..0000000000 --- a/tests/components/mipi_spi/test-s3boxlite.esp32-s3-idf.yaml +++ /dev/null @@ -1,41 +0,0 @@ -spi: - - id: quad_spi - type: quad - interface: spi3 - clk_pin: - number: 47 - data_pins: - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: octal_spi - type: octal - interface: hardware - clk_pin: - number: 0 - data_pins: - - 36 - - 37 - - 38 - - 39 - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: spi_id_3 - interface: any - clk_pin: 8 - mosi_pin: 9 - -display: - - platform: mipi_spi - model: S3BOXLITE diff --git a/tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml deleted file mode 100644 index dc328f950c..0000000000 --- a/tests/components/mipi_spi/test-t-display-s3-amoled-plus.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -spi: - - id: spi_id_3 - interface: any - clk_pin: 8 - mosi_pin: 9 - -display: - - platform: mipi_spi - model: T-DISPLAY-S3-AMOLED-PLUS diff --git a/tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml deleted file mode 100644 index f0432270dc..0000000000 --- a/tests/components/mipi_spi/test-t-display-s3-amoled.esp32-s3-idf.yaml +++ /dev/null @@ -1,15 +0,0 @@ -spi: - - id: quad_spi - type: quad - interface: spi3 - clk_pin: - number: 47 - data_pins: - - number: 40 - - number: 41 - - number: 42 - - number: 43 - -display: - - platform: mipi_spi - model: T-DISPLAY-S3-AMOLED diff --git a/tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml deleted file mode 100644 index 5cda38e096..0000000000 --- a/tests/components/mipi_spi/test-t-display-s3-pro.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -spi: - - id: spi_id_3 - interface: any - clk_pin: 8 - mosi_pin: 40 - -display: - - platform: mipi_spi - model: T-DISPLAY-S3-PRO diff --git a/tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml deleted file mode 100644 index 144bde8366..0000000000 --- a/tests/components/mipi_spi/test-t-display-s3.esp32-s3-idf.yaml +++ /dev/null @@ -1,37 +0,0 @@ -spi: - - id: quad_spi - type: quad - interface: spi3 - clk_pin: - number: 47 - data_pins: - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: octal_spi - type: octal - interface: hardware - clk_pin: - number: 0 - data_pins: - - 36 - - 37 - - 38 - - 39 - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - -display: - - platform: mipi_spi - model: T-DISPLAY-S3 diff --git a/tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml deleted file mode 100644 index 39339b5ae2..0000000000 --- a/tests/components/mipi_spi/test-t-display.esp32-s3-idf.yaml +++ /dev/null @@ -1,41 +0,0 @@ -spi: - - id: quad_spi - type: quad - interface: spi3 - clk_pin: - number: 47 - data_pins: - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: octal_spi - type: octal - interface: hardware - clk_pin: - number: 0 - data_pins: - - 36 - - 37 - - 38 - - 39 - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: spi_id_3 - interface: any - clk_pin: 8 - mosi_pin: 9 - -display: - - platform: mipi_spi - model: T-DISPLAY diff --git a/tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml deleted file mode 100644 index 6c9edb25b3..0000000000 --- a/tests/components/mipi_spi/test-t-embed.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -spi: - - id: spi_id_3 - interface: any - clk_pin: 8 - mosi_pin: 40 - -display: - - platform: mipi_spi - model: T-EMBED diff --git a/tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml deleted file mode 100644 index 46eaedb7cb..0000000000 --- a/tests/components/mipi_spi/test-t4-s3.esp32-s3-idf.yaml +++ /dev/null @@ -1,41 +0,0 @@ -spi: - - id: quad_spi - type: quad - interface: spi3 - clk_pin: - number: 47 - data_pins: - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: octal_spi - type: octal - interface: hardware - clk_pin: - number: 0 - data_pins: - - 36 - - 37 - - 38 - - 39 - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: spi_id_3 - interface: any - clk_pin: 8 - mosi_pin: 9 - -display: - - platform: mipi_spi - model: T4-S3 diff --git a/tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml deleted file mode 100644 index 3efb05ec89..0000000000 --- a/tests/components/mipi_spi/test-wt32-sc01-plus.esp32-s3-idf.yaml +++ /dev/null @@ -1,37 +0,0 @@ -spi: - - id: quad_spi - type: quad - interface: spi3 - clk_pin: - number: 47 - data_pins: - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - - id: octal_spi - type: octal - interface: hardware - clk_pin: - number: 9 - data_pins: - - 36 - - 37 - - 38 - - 39 - - allow_other_uses: true - number: 40 - - allow_other_uses: true - number: 41 - - allow_other_uses: true - number: 42 - - allow_other_uses: true - number: 43 - -display: - - platform: mipi_spi - model: WT32-SC01-PLUS diff --git a/tests/components/runtime_stats/common.yaml b/tests/components/runtime_stats/common.yaml new file mode 100644 index 0000000000..b434d1b5a7 --- /dev/null +++ b/tests/components/runtime_stats/common.yaml @@ -0,0 +1,2 @@ +# Test runtime_stats component with default configuration +runtime_stats: diff --git a/tests/components/runtime_stats/test.esp32-ard.yaml b/tests/components/runtime_stats/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/runtime_stats/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/time/test.nrf52-adafruit.yaml b/tests/components/time/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..a5502f8028 --- /dev/null +++ b/tests/components/time/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +time: diff --git a/tests/components/time/test.nrf52-mcumgr.yaml b/tests/components/time/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..a5502f8028 --- /dev/null +++ b/tests/components/time/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +time: diff --git a/tests/components/uptime/test.nrf52-adafruit.yaml b/tests/components/uptime/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..3c3c814813 --- /dev/null +++ b/tests/components/uptime/test.nrf52-adafruit.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: uptime + name: Uptime Sensor + - platform: uptime + name: Uptime Sensor Seconds + type: seconds + +text_sensor: + - platform: uptime + name: Uptime Text diff --git a/tests/components/uptime/test.nrf52-mcumgr.yaml b/tests/components/uptime/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..3c3c814813 --- /dev/null +++ b/tests/components/uptime/test.nrf52-mcumgr.yaml @@ -0,0 +1,10 @@ +sensor: + - platform: uptime + name: Uptime Sensor + - platform: uptime + name: Uptime Sensor Seconds + type: seconds + +text_sensor: + - platform: uptime + name: Uptime Text diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index e3ba09de43..6e2f398f49 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -395,7 +395,7 @@ async def wait_and_connect_api_client( # Wait for connection with timeout try: await asyncio.wait_for(connected_future, timeout=timeout) - except asyncio.TimeoutError: + except TimeoutError: raise TimeoutError(f"Failed to connect to API after {timeout} seconds") yield client @@ -575,12 +575,12 @@ async def run_binary_and_wait_for_port( process.send_signal(signal.SIGINT) try: await asyncio.wait_for(process.wait(), timeout=SIGINT_TIMEOUT) - except asyncio.TimeoutError: + except TimeoutError: # If SIGINT didn't work, try SIGTERM process.terminate() try: await asyncio.wait_for(process.wait(), timeout=SIGTERM_TIMEOUT) - except asyncio.TimeoutError: + except TimeoutError: # Last resort: SIGKILL process.kill() await process.wait() diff --git a/tests/integration/fixtures/api_string_lambda.yaml b/tests/integration/fixtures/api_string_lambda.yaml new file mode 100644 index 0000000000..e2da4683c0 --- /dev/null +++ b/tests/integration/fixtures/api_string_lambda.yaml @@ -0,0 +1,87 @@ +esphome: + name: api-string-lambda-test +host: + +api: + actions: + # Service that tests string lambda functionality + - action: test_string_lambda + variables: + input_string: string + then: + # Log the input to verify service was called + - logger.log: + format: "Service called with string: %s" + args: [input_string.c_str()] + + # This is the key test - using a lambda that returns x.c_str() + # where x is already a string. This would fail to compile in 2025.7.0b5 + # with "no matching function for call to 'to_string(std::string)'" + # This is the exact case from issue #9539 + - homeassistant.tag_scanned: !lambda 'return input_string.c_str();' + + # Also test with homeassistant.event to verify our fix works with data fields + - homeassistant.event: + event: esphome.test_string_lambda + data: + value: !lambda 'return input_string.c_str();' + + # Service that tests int lambda functionality + - action: test_int_lambda + variables: + input_number: int + then: + # Log the input to verify service was called + - logger.log: + format: "Service called with int: %d" + args: [input_number] + + # Test that int lambdas still work correctly with to_string + # The TemplatableStringValue should automatically convert int to string + - homeassistant.event: + event: esphome.test_int_lambda + data: + value: !lambda 'return input_number;' + + # Service that tests float lambda functionality + - action: test_float_lambda + variables: + input_float: float + then: + # Log the input to verify service was called + - logger.log: + format: "Service called with float: %.2f" + args: [input_float] + + # Test that float lambdas still work correctly with to_string + # The TemplatableStringValue should automatically convert float to string + - homeassistant.event: + event: esphome.test_float_lambda + data: + value: !lambda 'return input_float;' + + # Service that tests char* lambda functionality (e.g., from itoa or sprintf) + - action: test_char_ptr_lambda + variables: + input_number: int + input_string: string + then: + # Log the input to verify service was called + - logger.log: + format: "Service called with number for char* test: %d" + args: [input_number] + + # Test that char* lambdas work correctly + # This would fail in issue #9628 with "invalid conversion from 'char*' to 'long long unsigned int'" + - homeassistant.event: + event: esphome.test_char_ptr_lambda + data: + # Test snprintf returning char* + decimal_value: !lambda 'static char buffer[20]; snprintf(buffer, sizeof(buffer), "%d", input_number); return buffer;' + # Test strdup returning char* (dynamically allocated) + string_copy: !lambda 'return strdup(input_string.c_str());' + # Test string literal (const char*) + literal: !lambda 'return "test literal";' + +logger: + level: DEBUG diff --git a/tests/integration/fixtures/areas_and_devices.yaml b/tests/integration/fixtures/areas_and_devices.yaml index 6bf1519c79..12ab070e55 100644 --- a/tests/integration/fixtures/areas_and_devices.yaml +++ b/tests/integration/fixtures/areas_and_devices.yaml @@ -54,3 +54,45 @@ sensor: device_id: smart_switch_device lambda: return 4.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) + - platform: template + name: Test Switch + id: test_switch_main + optimistic: true + turn_on_action: + - logger.log: "Turning on Test Switch on Main Device (no device_id)" + turn_off_action: + - logger.log: "Turning off Test Switch on Main Device (no device_id)" + + - platform: template + name: Test Switch + device_id: light_controller_device + id: test_switch_light_controller + optimistic: true + turn_on_action: + - logger.log: "Turning on Test Switch on Light Controller" + turn_off_action: + - logger.log: "Turning off Test Switch on Light Controller" + + - platform: template + name: Test Switch + device_id: temp_sensor_device + id: test_switch_temp_sensor + optimistic: true + turn_on_action: + - logger.log: "Turning on Test Switch on Temperature Sensor" + turn_off_action: + - logger.log: "Turning off Test Switch on Temperature Sensor" + + - platform: template + name: Test Switch + device_id: motion_detector_device + id: test_switch_motion_detector + optimistic: true + turn_on_action: + - logger.log: "Turning on Test Switch on Motion Detector" + turn_off_action: + - logger.log: "Turning off Test Switch on Motion Detector" diff --git a/tests/integration/fixtures/delay_action_cancellation.yaml b/tests/integration/fixtures/delay_action_cancellation.yaml new file mode 100644 index 0000000000..e0dd427c2d --- /dev/null +++ b/tests/integration/fixtures/delay_action_cancellation.yaml @@ -0,0 +1,24 @@ +esphome: + name: test-delay-action + +host: +api: + actions: + - action: start_delay_then_restart + then: + - logger.log: "Starting first script execution" + - script.execute: test_delay_script + - delay: 250ms # Give first script time to start delay + - logger.log: "Restarting script (should cancel first delay)" + - script.execute: test_delay_script + +logger: + level: DEBUG + +script: + - id: test_delay_script + mode: restart + then: + - logger.log: "Script started, beginning delay" + - delay: 500ms # Long enough that it won't complete before restart + - logger.log: "Delay completed successfully" diff --git a/tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml b/tests/integration/fixtures/duplicate_entities_on_different_devices.yaml similarity index 61% rename from tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml rename to tests/integration/fixtures/duplicate_entities_on_different_devices.yaml index f7d017a0ae..ecc502ad28 100644 --- a/tests/integration/fixtures/duplicate_entities_not_allowed_on_different_devices.yaml +++ b/tests/integration/fixtures/duplicate_entities_on_different_devices.yaml @@ -1,6 +1,6 @@ esphome: name: duplicate-entities-test - # Define devices to test multi-device unique name validation + # Define devices to test multi-device duplicate handling devices: - id: controller_1 name: Controller 1 @@ -13,31 +13,31 @@ host: api: # Port will be automatically injected logger: -# Test that duplicate entity names are NOT allowed on different devices +# Test that duplicate entity names are allowed on different devices -# Scenario 1: Different sensor names on different devices (allowed) +# Scenario 1: Same sensor name on different devices (allowed) sensor: - platform: template - name: Temperature Controller 1 + name: Temperature device_id: controller_1 lambda: return 21.0; update_interval: 0.1s - platform: template - name: Temperature Controller 2 + name: Temperature device_id: controller_2 lambda: return 22.0; update_interval: 0.1s - platform: template - name: Temperature Controller 3 + name: Temperature device_id: controller_3 lambda: return 23.0; update_interval: 0.1s # Main device sensor (no device_id) - platform: template - name: Temperature Main + name: Temperature lambda: return 20.0; update_interval: 0.1s @@ -47,20 +47,20 @@ sensor: lambda: return 60.0; update_interval: 0.1s -# Scenario 2: Different binary sensor names on different devices +# Scenario 2: Same binary sensor name on different devices (allowed) binary_sensor: - platform: template - name: Status Controller 1 + name: Status device_id: controller_1 lambda: return true; - platform: template - name: Status Controller 2 + name: Status device_id: controller_2 lambda: return false; - platform: template - name: Status Main + name: Status lambda: return true; # Main device # Different platform can have same name as sensor @@ -68,43 +68,43 @@ binary_sensor: name: Temperature lambda: return true; -# Scenario 3: Different text sensor names on different devices +# Scenario 3: Same text sensor name on different devices text_sensor: - platform: template - name: Device Info Controller 1 + name: Device Info device_id: controller_1 lambda: return {"Controller 1 Active"}; update_interval: 0.1s - platform: template - name: Device Info Controller 2 + name: Device Info device_id: controller_2 lambda: return {"Controller 2 Active"}; update_interval: 0.1s - platform: template - name: Device Info Main + name: Device Info lambda: return {"Main Device Active"}; update_interval: 0.1s -# Scenario 4: Different switch names on different devices +# Scenario 4: Same switch name on different devices switch: - platform: template - name: Power Controller 1 + name: Power device_id: controller_1 lambda: return false; turn_on_action: [] turn_off_action: [] - platform: template - name: Power Controller 2 + name: Power device_id: controller_2 lambda: return true; turn_on_action: [] turn_off_action: [] - platform: template - name: Power Controller 3 + name: Power device_id: controller_3 lambda: return false; turn_on_action: [] @@ -117,54 +117,26 @@ switch: turn_on_action: [] turn_off_action: [] -# Scenario 5: Buttons with unique names +# Scenario 5: Empty names on different devices (should use device name) button: - platform: template - name: "Reset Controller 1" + name: "" device_id: controller_1 on_press: [] - platform: template - name: "Reset Controller 2" + name: "" device_id: controller_2 on_press: [] - platform: template - name: "Reset Main" + name: "" on_press: [] # Main device -# Scenario 6: Empty names (should use device names) -select: - - platform: template - name: "" - device_id: controller_1 - options: - - "Option 1" - - "Option 2" - lambda: return {"Option 1"}; - set_action: [] - - - platform: template - name: "" - device_id: controller_2 - options: - - "Option 1" - - "Option 2" - lambda: return {"Option 1"}; - set_action: [] - - - platform: template - name: "" # Main device - options: - - "Option 1" - - "Option 2" - lambda: return {"Option 1"}; - set_action: [] - -# Scenario 7: Special characters in names - now with unique names +# Scenario 6: Special characters in names number: - platform: template - name: "Temperature Setpoint! Controller 1" + name: "Temperature Setpoint!" device_id: controller_1 min_value: 10.0 max_value: 30.0 @@ -173,7 +145,7 @@ number: set_action: [] - platform: template - name: "Temperature Setpoint! Controller 2" + name: "Temperature Setpoint!" device_id: controller_2 min_value: 10.0 max_value: 30.0 diff --git a/tests/integration/fixtures/host_mode_api_password.yaml b/tests/integration/fixtures/host_mode_api_password.yaml new file mode 100644 index 0000000000..038b6871e0 --- /dev/null +++ b/tests/integration/fixtures/host_mode_api_password.yaml @@ -0,0 +1,14 @@ +esphome: + name: host-mode-api-password +host: +api: + password: "test_password_123" +logger: + level: DEBUG +# Test sensor to verify connection works +sensor: + - platform: template + name: Test Sensor + id: test_sensor + lambda: return 42.0; + update_interval: 0.1s diff --git a/tests/integration/fixtures/runtime_stats.yaml b/tests/integration/fixtures/runtime_stats.yaml new file mode 100644 index 0000000000..aad1c275fb --- /dev/null +++ b/tests/integration/fixtures/runtime_stats.yaml @@ -0,0 +1,39 @@ +esphome: + name: runtime-stats-test + +host: + +api: + +logger: + level: DEBUG + logs: + runtime_stats: INFO + +runtime_stats: + log_interval: 1s + +# Add some components that will execute periodically to generate stats +sensor: + - platform: template + name: "Test Sensor 1" + id: test_sensor_1 + lambda: return 42.0; + update_interval: 0.1s + + - platform: template + name: "Test Sensor 2" + id: test_sensor_2 + lambda: return 24.0; + update_interval: 0.2s + +switch: + - platform: template + name: "Test Switch" + id: test_switch + optimistic: true + +interval: + - interval: 0.5s + then: + - switch.toggle: test_switch diff --git a/tests/integration/fixtures/scheduler_retry_test.yaml b/tests/integration/fixtures/scheduler_retry_test.yaml new file mode 100644 index 0000000000..bae50e9ed7 --- /dev/null +++ b/tests/integration/fixtures/scheduler_retry_test.yaml @@ -0,0 +1,207 @@ +esphome: + name: scheduler-retry-test + on_boot: + priority: -100 + then: + - logger.log: "Starting scheduler retry tests" + # Run all tests sequentially with delays + - script.execute: run_all_tests + +host: +api: +logger: + level: VERBOSE + +globals: + - id: simple_retry_counter + type: int + initial_value: '0' + - id: backoff_retry_counter + type: int + initial_value: '0' + - id: immediate_done_counter + type: int + initial_value: '0' + - id: cancel_retry_counter + type: int + initial_value: '0' + - id: empty_name_retry_counter + type: int + initial_value: '0' + - id: script_retry_counter + type: int + initial_value: '0' + - id: multiple_same_name_counter + type: int + initial_value: '0' + +sensor: + - platform: template + name: Test Sensor + id: test_sensor + lambda: return 1.0; + update_interval: never + +script: + - id: run_all_tests + then: + # Test 1: Simple retry + - logger.log: "=== Test 1: Simple retry ===" + - lambda: |- + auto *component = id(test_sensor); + App.scheduler.set_retry(component, "simple_retry", 50, 3, + [](uint8_t retry_countdown) { + id(simple_retry_counter)++; + ESP_LOGI("test", "Simple retry attempt %d (countdown=%d)", + id(simple_retry_counter), retry_countdown); + + if (id(simple_retry_counter) >= 2) { + ESP_LOGI("test", "Simple retry succeeded on attempt %d", id(simple_retry_counter)); + return RetryResult::DONE; + } + return RetryResult::RETRY; + }); + + # Test 2: Backoff retry + - logger.log: "=== Test 2: Retry with backoff ===" + - lambda: |- + auto *component = id(test_sensor); + static uint32_t backoff_start_time = 0; + static uint32_t last_attempt_time = 0; + + backoff_start_time = millis(); + last_attempt_time = backoff_start_time; + + App.scheduler.set_retry(component, "backoff_retry", 50, 4, + [](uint8_t retry_countdown) { + id(backoff_retry_counter)++; + uint32_t now = millis(); + uint32_t interval = now - last_attempt_time; + last_attempt_time = now; + + ESP_LOGI("test", "Backoff retry attempt %d (countdown=%d, interval=%dms)", + id(backoff_retry_counter), retry_countdown, interval); + + if (id(backoff_retry_counter) == 1) { + ESP_LOGI("test", "First call was immediate"); + } else if (id(backoff_retry_counter) == 2) { + ESP_LOGI("test", "Second call interval: %dms (expected ~50ms)", interval); + } else if (id(backoff_retry_counter) == 3) { + ESP_LOGI("test", "Third call interval: %dms (expected ~100ms)", interval); + } else if (id(backoff_retry_counter) == 4) { + ESP_LOGI("test", "Fourth call interval: %dms (expected ~200ms)", interval); + ESP_LOGI("test", "Backoff retry completed"); + return RetryResult::DONE; + } + + return RetryResult::RETRY; + }, 2.0f); + + # Test 3: Immediate done + - logger.log: "=== Test 3: Immediate done ===" + - lambda: |- + auto *component = id(test_sensor); + App.scheduler.set_retry(component, "immediate_done", 50, 5, + [](uint8_t retry_countdown) { + id(immediate_done_counter)++; + ESP_LOGI("test", "Immediate done retry called (countdown=%d)", retry_countdown); + return RetryResult::DONE; + }); + + # Test 4: Cancel retry + - logger.log: "=== Test 4: Cancel retry ===" + - lambda: |- + auto *component = id(test_sensor); + App.scheduler.set_retry(component, "cancel_test", 25, 10, + [](uint8_t retry_countdown) { + id(cancel_retry_counter)++; + ESP_LOGI("test", "Cancel test retry attempt %d", id(cancel_retry_counter)); + return RetryResult::RETRY; + }); + + // Cancel it after 100ms + App.scheduler.set_timeout(component, "cancel_timer", 100, []() { + bool cancelled = App.scheduler.cancel_retry(id(test_sensor), "cancel_test"); + ESP_LOGI("test", "Retry cancellation result: %s", cancelled ? "true" : "false"); + ESP_LOGI("test", "Cancel retry ran %d times before cancellation", id(cancel_retry_counter)); + }); + + # Test 5: Empty name retry + - logger.log: "=== Test 5: Empty name retry ===" + - lambda: |- + auto *component = id(test_sensor); + App.scheduler.set_retry(component, "", 50, 5, + [](uint8_t retry_countdown) { + id(empty_name_retry_counter)++; + ESP_LOGI("test", "Empty name retry attempt %d", id(empty_name_retry_counter)); + return RetryResult::RETRY; + }); + + // Try to cancel after 75ms + App.scheduler.set_timeout(component, "empty_cancel_timer", 75, []() { + bool cancelled = App.scheduler.cancel_retry(id(test_sensor), ""); + ESP_LOGI("test", "Empty name retry cancel result: %s", + cancelled ? "true" : "false"); + ESP_LOGI("test", "Empty name retry ran %d times", id(empty_name_retry_counter)); + }); + + # Test 6: Component method + - logger.log: "=== Test 6: Component::set_retry method ===" + - lambda: |- + class TestRetryComponent : public Component { + public: + void test_retry() { + this->set_retry(50, 3, + [](uint8_t retry_countdown) { + id(script_retry_counter)++; + ESP_LOGI("test", "Component retry attempt %d", id(script_retry_counter)); + if (id(script_retry_counter) >= 2) { + return RetryResult::DONE; + } + return RetryResult::RETRY; + }, 1.5f); + } + }; + + static TestRetryComponent test_component; + test_component.test_retry(); + + # Test 7: Multiple same name + - logger.log: "=== Test 7: Multiple retries with same name ===" + - lambda: |- + auto *component = id(test_sensor); + + // Set first retry + App.scheduler.set_retry(component, "duplicate_retry", 100, 5, + [](uint8_t retry_countdown) { + id(multiple_same_name_counter) += 1; + ESP_LOGI("test", "First duplicate retry - should not run"); + return RetryResult::RETRY; + }); + + // Set second retry with same name (should cancel first) + App.scheduler.set_retry(component, "duplicate_retry", 50, 3, + [](uint8_t retry_countdown) { + id(multiple_same_name_counter) += 10; + ESP_LOGI("test", "Second duplicate retry attempt (counter=%d)", + id(multiple_same_name_counter)); + if (id(multiple_same_name_counter) >= 20) { + return RetryResult::DONE; + } + return RetryResult::RETRY; + }); + + # Wait for all tests to complete before reporting + - delay: 500ms + + # Final report + - logger.log: "=== Retry Test Results ===" + - lambda: |- + ESP_LOGI("test", "Simple retry counter: %d (expected 2)", id(simple_retry_counter)); + ESP_LOGI("test", "Backoff retry counter: %d (expected 4)", id(backoff_retry_counter)); + ESP_LOGI("test", "Immediate done counter: %d (expected 1)", id(immediate_done_counter)); + ESP_LOGI("test", "Cancel retry counter: %d (expected ~3-4)", id(cancel_retry_counter)); + ESP_LOGI("test", "Empty name retry counter: %d (expected 1-2)", id(empty_name_retry_counter)); + ESP_LOGI("test", "Component retry counter: %d (expected 2)", id(script_retry_counter)); + ESP_LOGI("test", "Multiple same name counter: %d (expected 20+)", id(multiple_same_name_counter)); + ESP_LOGI("test", "All retry tests completed"); diff --git a/tests/integration/fixtures/scheduler_string_test.yaml b/tests/integration/fixtures/scheduler_string_test.yaml index 3dfe891370..c53ec392df 100644 --- a/tests/integration/fixtures/scheduler_string_test.yaml +++ b/tests/integration/fixtures/scheduler_string_test.yaml @@ -4,9 +4,7 @@ esphome: priority: -100 then: - logger.log: "Starting scheduler string tests" - platformio_options: - build_flags: - - "-DESPHOME_DEBUG_SCHEDULER" # Enable scheduler debug logging + debug_scheduler: true # Enable scheduler debug logging host: api: @@ -32,6 +30,12 @@ globals: - id: results_reported type: bool initial_value: 'false' + - id: edge_tests_done + type: bool + initial_value: 'false' + - id: empty_cancel_failed + type: bool + initial_value: 'false' script: - id: test_static_strings @@ -147,12 +151,106 @@ script: static TestDynamicDeferComponent test_dynamic_defer_component; test_dynamic_defer_component.test_dynamic_defer(); + - id: test_cancellation_edge_cases + then: + - logger.log: "Testing cancellation edge cases" + - lambda: |- + auto *component1 = id(test_sensor1); + // Use a different component for empty string tests to avoid interference + auto *component2 = id(test_sensor2); + + // Test 12: Cancel with empty string - regression test for issue #9599 + // First create a timeout with empty name on component2 to avoid interference + App.scheduler.set_timeout(component2, "", 500, []() { + ESP_LOGE("test", "ERROR: Empty name timeout fired - it should have been cancelled!"); + id(empty_cancel_failed) = true; + }); + + // Now cancel it - this should work after our fix + bool cancelled_empty = App.scheduler.cancel_timeout(component2, ""); + ESP_LOGI("test", "Cancel empty string result: %s (should be true)", cancelled_empty ? "true" : "false"); + if (!cancelled_empty) { + ESP_LOGE("test", "ERROR: Failed to cancel empty string timeout!"); + id(empty_cancel_failed) = true; + } + + // Test 13: Cancel non-existent timeout + bool cancelled_nonexistent = App.scheduler.cancel_timeout(component1, "does_not_exist"); + ESP_LOGI("test", "Cancel non-existent timeout result: %s", + cancelled_nonexistent ? "true (unexpected!)" : "false (expected)"); + + // Test 14: Multiple timeouts with same name - only last should execute + for (int i = 0; i < 5; i++) { + App.scheduler.set_timeout(component1, "duplicate_timeout", 200 + i*10, [i]() { + ESP_LOGI("test", "Duplicate timeout %d fired", i); + id(timeout_counter) += 1; + }); + } + ESP_LOGI("test", "Created 5 timeouts with same name 'duplicate_timeout'"); + + // Test 15: Multiple intervals with same name - only last should run + for (int i = 0; i < 3; i++) { + App.scheduler.set_interval(component1, "duplicate_interval", 300, [i]() { + ESP_LOGI("test", "Duplicate interval %d fired", i); + id(interval_counter) += 10; // Large increment to detect multiple + // Cancel after first execution + App.scheduler.cancel_interval(id(test_sensor1), "duplicate_interval"); + }); + } + ESP_LOGI("test", "Created 3 intervals with same name 'duplicate_interval'"); + + // Test 16: Cancel with nullptr protection (via empty const char*) + const char* null_name = ""; + App.scheduler.set_timeout(component2, null_name, 600, []() { + ESP_LOGE("test", "ERROR: Const char* empty timeout fired - should have been cancelled!"); + id(empty_cancel_failed) = true; + }); + bool cancelled_const_empty = App.scheduler.cancel_timeout(component2, null_name); + ESP_LOGI("test", "Cancel const char* empty result: %s (should be true)", + cancelled_const_empty ? "true" : "false"); + if (!cancelled_const_empty) { + ESP_LOGE("test", "ERROR: Failed to cancel const char* empty timeout!"); + id(empty_cancel_failed) = true; + } + + // Test 17: Rapid create/cancel/create with same name + App.scheduler.set_timeout(component1, "rapid_test", 5000, []() { + ESP_LOGI("test", "First rapid timeout - should not fire"); + id(timeout_counter) += 100; + }); + App.scheduler.cancel_timeout(component1, "rapid_test"); + App.scheduler.set_timeout(component1, "rapid_test", 250, []() { + ESP_LOGI("test", "Second rapid timeout - should fire"); + id(timeout_counter) += 1; + }); + + // Test 18: Cancel all with a specific name (multiple instances) + // Create multiple with same name + App.scheduler.set_timeout(component1, "multi_cancel", 300, []() { + ESP_LOGI("test", "Multi-cancel timeout 1"); + }); + App.scheduler.set_timeout(component1, "multi_cancel", 350, []() { + ESP_LOGI("test", "Multi-cancel timeout 2"); + }); + App.scheduler.set_timeout(component1, "multi_cancel", 400, []() { + ESP_LOGI("test", "Multi-cancel timeout 3 - only this should fire"); + id(timeout_counter) += 1; + }); + // Note: Each set_timeout with same name cancels the previous one automatically + - id: report_results then: - lambda: |- ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d", id(timeout_counter), id(interval_counter)); + // Check if empty string cancellation test passed + if (id(empty_cancel_failed)) { + ESP_LOGE("test", "ERROR: Empty string cancellation test FAILED!"); + } else { + ESP_LOGI("test", "Empty string cancellation test PASSED"); + } + sensor: - platform: template name: Test Sensor 1 @@ -189,12 +287,23 @@ interval: - delay: 0.2s - script.execute: test_dynamic_strings + # Run cancellation edge case tests after dynamic tests + - interval: 0.2s + then: + - if: + condition: + lambda: 'return id(dynamic_tests_done) && !id(edge_tests_done);' + then: + - lambda: 'id(edge_tests_done) = true;' + - delay: 0.5s + - script.execute: test_cancellation_edge_cases + # Report results after all tests - interval: 0.2s then: - if: condition: - lambda: 'return id(dynamic_tests_done) && !id(results_reported);' + lambda: 'return id(edge_tests_done) && !id(results_reported);' then: - lambda: 'id(results_reported) = true;' - delay: 1s diff --git a/tests/integration/test_api_message_size_batching.py b/tests/integration/test_api_message_size_batching.py index 631e64825e..f7859eb902 100644 --- a/tests/integration/test_api_message_size_batching.py +++ b/tests/integration/test_api_message_size_batching.py @@ -177,7 +177,7 @@ async def test_api_message_size_batching( # Wait for states with timeout try: await asyncio.wait_for(states_future, timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: missing_keys = expected_keys - received_keys pytest.fail( f"Did not receive states from all entities within 5 seconds. " diff --git a/tests/integration/test_api_reboot_timeout.py b/tests/integration/test_api_reboot_timeout.py index dd9f5fbd1e..9cada0a296 100644 --- a/tests/integration/test_api_reboot_timeout.py +++ b/tests/integration/test_api_reboot_timeout.py @@ -29,7 +29,7 @@ async def test_api_reboot_timeout( # (0.5s reboot timeout + some margin for processing) try: await asyncio.wait_for(reboot_future, timeout=2.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Device did not reboot within expected timeout") # Test passes if we get here - reboot was detected diff --git a/tests/integration/test_api_string_lambda.py b/tests/integration/test_api_string_lambda.py new file mode 100644 index 0000000000..f4ef77bad8 --- /dev/null +++ b/tests/integration/test_api_string_lambda.py @@ -0,0 +1,100 @@ +"""Integration test for TemplatableStringValue with string lambdas.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_string_lambda( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test TemplatableStringValue works with lambdas that return different types.""" + loop = asyncio.get_running_loop() + + # Track log messages for all four service calls + string_called_future = loop.create_future() + int_called_future = loop.create_future() + float_called_future = loop.create_future() + char_ptr_called_future = loop.create_future() + + # Patterns to match in logs - confirms the lambdas compiled and executed + string_pattern = re.compile(r"Service called with string: STRING_FROM_LAMBDA") + int_pattern = re.compile(r"Service called with int: 42") + float_pattern = re.compile(r"Service called with float: 3\.14") + char_ptr_pattern = re.compile(r"Service called with number for char\* test: 123") + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if not string_called_future.done() and string_pattern.search(line): + string_called_future.set_result(True) + if not int_called_future.done() and int_pattern.search(line): + int_called_future.set_result(True) + if not float_called_future.done() and float_pattern.search(line): + float_called_future.set_result(True) + if not char_ptr_called_future.done() and char_ptr_pattern.search(line): + char_ptr_called_future.set_result(True) + + # 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 == "api-string-lambda-test" + + # List services to find our test services + _, services = await client.list_entities_services() + + # Find all test services + string_service = next( + (s for s in services if s.name == "test_string_lambda"), None + ) + assert string_service is not None, "test_string_lambda service not found" + + int_service = next((s for s in services if s.name == "test_int_lambda"), None) + assert int_service is not None, "test_int_lambda service not found" + + float_service = next( + (s for s in services if s.name == "test_float_lambda"), None + ) + assert float_service is not None, "test_float_lambda service not found" + + char_ptr_service = next( + (s for s in services if s.name == "test_char_ptr_lambda"), None + ) + assert char_ptr_service is not None, "test_char_ptr_lambda service not found" + + # Execute all four services to test different lambda return types + client.execute_service(string_service, {"input_string": "STRING_FROM_LAMBDA"}) + client.execute_service(int_service, {"input_number": 42}) + client.execute_service(float_service, {"input_float": 3.14}) + client.execute_service( + char_ptr_service, {"input_number": 123, "input_string": "test_string"} + ) + + # Wait for all service log messages + # This confirms the lambdas compiled successfully and executed + try: + await asyncio.wait_for( + asyncio.gather( + string_called_future, + int_called_future, + float_called_future, + char_ptr_called_future, + ), + timeout=5.0, + ) + except TimeoutError: + pytest.fail( + "One or more service log messages not received - lambda may have failed to compile or execute" + ) diff --git a/tests/integration/test_areas_and_devices.py b/tests/integration/test_areas_and_devices.py index 4184255724..1af16c87e8 100644 --- a/tests/integration/test_areas_and_devices.py +++ b/tests/integration/test_areas_and_devices.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio -from aioesphomeapi import EntityState +from aioesphomeapi import EntityState, SwitchInfo, SwitchState import pytest from .types import APIClientConnectedFactory, RunCompiledFunction @@ -84,23 +84,45 @@ async def test_areas_and_devices( # Subscribe to states to get sensor values loop = asyncio.get_running_loop() - states: dict[int, EntityState] = {} - states_future: asyncio.Future[bool] = loop.create_future() + states: dict[tuple[int, int], EntityState] = {} + # Subscribe to all switch states + switch_state_futures: dict[ + tuple[int, int], asyncio.Future[EntityState] + ] = {} # (device_id, key) -> future + initial_states_received: set[tuple[int, int]] = set() + initial_states_future: asyncio.Future[bool] = loop.create_future() def on_state(state: EntityState) -> None: - states[state.key] = state - # Check if we have all expected sensor states - if len(states) >= 4 and not states_future.done(): - states_future.set_result(True) + state_key = (state.device_id, state.key) + states[state_key] = state + + initial_states_received.add(state_key) + # Check if we have all initial states + if ( + len(initial_states_received) >= 8 # 8 entities expected + and not initial_states_future.done() + ): + initial_states_future.set_result(True) + + if not initial_states_future.done(): + return + + # Resolve the future for this switch if it exists + if ( + state_key in switch_state_futures + and not switch_state_futures[state_key].done() + and isinstance(state, SwitchState) + ): + switch_state_futures[state_key].set_result(state) client.subscribe_states(on_state) # Wait for sensor states try: - await asyncio.wait_for(states_future, timeout=10.0) - except asyncio.TimeoutError: + await asyncio.wait_for(initial_states_future, timeout=10.0) + except TimeoutError: pytest.fail( - f"Did not receive all sensor states within 10 seconds. " + f"Did not receive all states within 10 seconds. " f"Received {len(states)} states" ) @@ -119,3 +141,121 @@ async def test_areas_and_devices( f"{entity.name} has device_id {entity.device_id}, " f"expected {expected_device_id}" ) + + all_entities, _ = entities # Unpack the tuple + switch_entities = [e for e in all_entities if isinstance(e, SwitchInfo)] + + # Find all switches named "Test Switch" + test_switches = [e for e in switch_entities if e.name == "Test Switch"] + assert len(test_switches) == 4, ( + f"Expected 4 'Test Switch' entities, got {len(test_switches)}" + ) + + # Verify we have switches with different device_ids including one with 0 (main) + switch_device_ids = {s.device_id for s in test_switches} + assert len(switch_device_ids) == 4, ( + "All Test Switch entities should have different device_ids" + ) + assert 0 in switch_device_ids, ( + "Should have a switch with 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) + + # Test controlling each switch specifically by device_id + for device_name, device in [ + ("Light Controller", light_controller), + ("Temperature Sensor", temp_sensor), + ("Motion Detector", motion_detector), + ]: + # Find the switch for this specific device + device_switch = next( + (s for s in test_switches if s.device_id == device.device_id), None + ) + assert device_switch is not None, f"No Test Switch found for {device_name}" + + # Create future for this switch's state change + state_key = (device_switch.device_id, device_switch.key) + switch_state_futures[state_key] = loop.create_future() + + # Turn on the switch with device_id + client.switch_command( + device_switch.key, True, device_id=device_switch.device_id + ) + + # Wait for state to change + await asyncio.wait_for(switch_state_futures[state_key], timeout=2.0) + + # Verify the correct switch was turned on + assert states[state_key].state is True, f"{device_name} switch should be on" + + # Create new future for turning off + switch_state_futures[state_key] = loop.create_future() + + # Turn off the switch with device_id + client.switch_command( + device_switch.key, False, device_id=device_switch.device_id + ) + + # Wait for state to change + await asyncio.wait_for(switch_state_futures[state_key], timeout=2.0) + + # Verify the correct switch was turned off + assert states[state_key].state is False, ( + f"{device_name} switch should be off" + ) + + # Test that controlling a switch with device_id doesn't affect main switch + # Find the main switch (device_id = 0) + main_switch = next((s for s in test_switches if s.device_id == 0), None) + assert main_switch is not None, "No main switch (device_id=0) found" + + # Find a switch with a device_id + device_switch = next( + (s for s in test_switches if s.device_id == light_controller.device_id), + None, + ) + assert device_switch is not None, "No device switch found" + + # Create futures for both switches + main_key = (main_switch.device_id, main_switch.key) + device_key = (device_switch.device_id, device_switch.key) + + # Turn on the main switch first + switch_state_futures[main_key] = loop.create_future() + client.switch_command(main_switch.key, True, device_id=main_switch.device_id) + await asyncio.wait_for(switch_state_futures[main_key], timeout=2.0) + assert states[main_key].state is True, "Main switch should be on" + + # Now turn on the device switch + switch_state_futures[device_key] = loop.create_future() + client.switch_command( + device_switch.key, True, device_id=device_switch.device_id + ) + await asyncio.wait_for(switch_state_futures[device_key], timeout=2.0) + + # Verify device switch is on and main switch is still on + assert states[device_key].state is True, "Device switch should be on" + assert states[main_key].state is True, ( + "Main switch should still be on after turning on device switch" + ) + + # Turn off the device switch + switch_state_futures[device_key] = loop.create_future() + client.switch_command( + device_switch.key, False, device_id=device_switch.device_id + ) + await asyncio.wait_for(switch_state_futures[device_key], timeout=2.0) + + # Verify device switch is off and main switch is still on + assert states[device_key].state is False, "Device switch should be off" + assert states[main_key].state is True, ( + "Main switch should still be on after turning off device switch" + ) + + # Clean up - turn off main switch + switch_state_futures[main_key] = loop.create_future() + client.switch_command(main_switch.key, False, device_id=main_switch.device_id) + await asyncio.wait_for(switch_state_futures[main_key], timeout=2.0) + assert states[main_key].state is False, "Main switch should be off" diff --git a/tests/integration/test_automations.py b/tests/integration/test_automations.py new file mode 100644 index 0000000000..bd2082e86b --- /dev/null +++ b/tests/integration/test_automations.py @@ -0,0 +1,91 @@ +"""Test ESPHome automations functionality.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_delay_action_cancellation( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that delay actions can be properly cancelled when script restarts.""" + loop = asyncio.get_running_loop() + + # Track log messages with timestamps + log_entries: list[tuple[float, str]] = [] + script_starts: list[float] = [] + delay_completions: list[float] = [] + script_restart_logged = False + test_started_time = None + + # Patterns to match + test_start_pattern = re.compile(r"Starting first script execution") + script_start_pattern = re.compile(r"Script started, beginning delay") + restart_pattern = re.compile(r"Restarting script \(should cancel first delay\)") + delay_complete_pattern = re.compile(r"Delay completed successfully") + + # Future to track when we can check results + second_script_started = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + nonlocal script_restart_logged, test_started_time + + current_time = loop.time() + log_entries.append((current_time, line)) + + if test_start_pattern.search(line): + test_started_time = current_time + elif script_start_pattern.search(line) and test_started_time: + script_starts.append(current_time) + if len(script_starts) == 2 and not second_script_started.done(): + second_script_started.set_result(True) + elif restart_pattern.search(line): + script_restart_logged = True + elif delay_complete_pattern.search(line): + delay_completions.append(current_time) + + 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 == "start_delay_then_restart"), None + ) + assert test_service is not None, "start_delay_then_restart service not found" + + # Execute the test sequence + client.execute_service(test_service, {}) + + # Wait for the second script to start + await asyncio.wait_for(second_script_started, timeout=5.0) + + # Wait for potential delay completion + await asyncio.sleep(0.75) # Original delay was 500ms + + # Check results + assert len(script_starts) == 2, ( + f"Script should have started twice, but started {len(script_starts)} times" + ) + assert script_restart_logged, "Script restart was not logged" + + # Verify we got exactly one completion and it happened ~500ms after the second start + assert len(delay_completions) == 1, ( + f"Expected 1 delay completion, got {len(delay_completions)}" + ) + time_from_second_start = delay_completions[0] - script_starts[1] + assert 0.4 < time_from_second_start < 0.6, ( + f"Delay completed {time_from_second_start:.3f}s after second start, expected ~0.5s" + ) diff --git a/tests/integration/test_device_id_in_state.py b/tests/integration/test_device_id_in_state.py index eaa91ec92e..fb61569e59 100644 --- a/tests/integration/test_device_id_in_state.py +++ b/tests/integration/test_device_id_in_state.py @@ -77,7 +77,7 @@ async def test_device_id_in_state( # Wait for states try: await asyncio.wait_for(states_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Did not receive all entity states within 10 seconds. " f"Received {len(states)} states, expected {len(entity_device_mapping)}" diff --git a/tests/integration/test_duplicate_entities.py b/tests/integration/test_duplicate_entities.py index b7ee8dd478..c738bb3fe0 100644 --- a/tests/integration/test_duplicate_entities.py +++ b/tests/integration/test_duplicate_entities.py @@ -11,12 +11,12 @@ from .types import APIClientConnectedFactory, RunCompiledFunction @pytest.mark.asyncio -async def test_duplicate_entities_not_allowed_on_different_devices( +async def test_duplicate_entities_on_different_devices( yaml_config: str, run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, ) -> None: - """Test that duplicate entity names are NOT allowed on different devices.""" + """Test that duplicate entity names are allowed on different devices.""" async with run_compiled(yaml_config), api_client_connected() as client: # Get device info device_info = await client.device_info() @@ -52,46 +52,42 @@ async def test_duplicate_entities_not_allowed_on_different_devices( switches = [e for e in all_entities if e.__class__.__name__ == "SwitchInfo"] buttons = [e for e in all_entities if e.__class__.__name__ == "ButtonInfo"] numbers = [e for e in all_entities if e.__class__.__name__ == "NumberInfo"] - selects = [e for e in all_entities if e.__class__.__name__ == "SelectInfo"] - # Scenario 1: Check that temperature sensors have unique names per device - temp_sensors = [s for s in sensors if "Temperature" in s.name] + # Scenario 1: Check sensors with same "Temperature" name on different devices + temp_sensors = [s for s in sensors if s.name == "Temperature"] assert len(temp_sensors) == 4, ( f"Expected exactly 4 temperature sensors, got {len(temp_sensors)}" ) - # Verify each sensor has a unique name - temp_names = set() + # Verify each sensor is on a different device + temp_device_ids = set() temp_object_ids = set() for sensor in temp_sensors: - temp_names.add(sensor.name) + temp_device_ids.add(sensor.device_id) temp_object_ids.add(sensor.object_id) - # Should have 4 unique names - assert len(temp_names) == 4, ( - f"Temperature sensors should have unique names, got {temp_names}" + # All should have object_id "temperature" (no suffix) + assert sensor.object_id == "temperature", ( + f"Expected object_id 'temperature', got '{sensor.object_id}'" + ) + + # Should have 4 different device IDs (including None for main device) + assert len(temp_device_ids) == 4, ( + f"Temperature sensors should be on different devices, got {temp_device_ids}" ) - # Object IDs should also be unique - assert len(temp_object_ids) == 4, ( - f"Temperature sensors should have unique object_ids, got {temp_object_ids}" - ) - - # Scenario 2: Check binary sensors have unique names - status_binary = [b for b in binary_sensors if "Status" in b.name] + # Scenario 2: Check binary sensors "Status" on different devices + status_binary = [b for b in binary_sensors if b.name == "Status"] assert len(status_binary) == 3, ( f"Expected exactly 3 status binary sensors, got {len(status_binary)}" ) - # All should have unique object_ids - status_names = set() + # All should have object_id "status" for binary in status_binary: - status_names.add(binary.name) - - assert len(status_names) == 3, ( - f"Status binary sensors should have unique names, got {status_names}" - ) + assert binary.object_id == "status", ( + f"Expected object_id 'status', got '{binary.object_id}'" + ) # Scenario 3: Check that sensor and binary_sensor can have same name temp_binary = [b for b in binary_sensors if b.name == "Temperature"] @@ -100,86 +96,62 @@ async def test_duplicate_entities_not_allowed_on_different_devices( ) assert temp_binary[0].object_id == "temperature" - # Scenario 4: Check text sensors have unique names - info_text = [t for t in text_sensors if "Device Info" in t.name] + # Scenario 4: Check text sensors "Device Info" on different devices + info_text = [t for t in text_sensors if t.name == "Device Info"] assert len(info_text) == 3, ( f"Expected exactly 3 device info text sensors, got {len(info_text)}" ) - # All should have unique names and object_ids - info_names = set() + # All should have object_id "device_info" for text in info_text: - info_names.add(text.name) + assert text.object_id == "device_info", ( + f"Expected object_id 'device_info', got '{text.object_id}'" + ) - assert len(info_names) == 3, ( - f"Device info text sensors should have unique names, got {info_names}" + # Scenario 5: Check switches "Power" on different devices + power_switches = [s for s in switches if s.name == "Power"] + assert len(power_switches) == 3, ( + f"Expected exactly 3 power switches, got {len(power_switches)}" ) - # Scenario 5: Check switches have unique names - power_switches = [s for s in switches if "Power" in s.name] - assert len(power_switches) == 4, ( - f"Expected exactly 4 power switches, got {len(power_switches)}" - ) - - # All should have unique names - power_names = set() + # All should have object_id "power" for switch in power_switches: - power_names.add(switch.name) + assert switch.object_id == "power", ( + f"Expected object_id 'power', got '{switch.object_id}'" + ) - assert len(power_names) == 4, ( - f"Power switches should have unique names, got {power_names}" - ) - - # Scenario 6: Check reset buttons have unique names - reset_buttons = [b for b in buttons if "Reset" in b.name] - assert len(reset_buttons) == 3, ( - f"Expected exactly 3 reset buttons, got {len(reset_buttons)}" - ) - - # All should have unique names - reset_names = set() - for button in reset_buttons: - reset_names.add(button.name) - - assert len(reset_names) == 3, ( - f"Reset buttons should have unique names, got {reset_names}" - ) - - # Scenario 7: Check empty name selects (should use device names) - empty_selects = [s for s in selects if s.name == ""] - assert len(empty_selects) == 3, ( - f"Expected exactly 3 empty name selects, got {len(empty_selects)}" + # Scenario 6: Check empty name buttons (should use device name) + empty_buttons = [b for b in buttons if b.name == ""] + assert len(empty_buttons) == 3, ( + f"Expected exactly 3 empty name buttons, got {len(empty_buttons)}" ) # Group by device - c1_selects = [s for s in empty_selects if s.device_id == controller_1.device_id] - c2_selects = [s for s in empty_selects if s.device_id == controller_2.device_id] + c1_buttons = [b for b in empty_buttons if b.device_id == controller_1.device_id] + c2_buttons = [b for b in empty_buttons if b.device_id == controller_2.device_id] # For main device, device_id is 0 - main_selects = [s for s in empty_selects if s.device_id == 0] + main_buttons = [b for b in empty_buttons if b.device_id == 0] - # Check object IDs for empty name entities - they should use device names - assert len(c1_selects) == 1 and c1_selects[0].object_id == "controller_1" - assert len(c2_selects) == 1 and c2_selects[0].object_id == "controller_2" + # Check object IDs for empty name entities + assert len(c1_buttons) == 1 and c1_buttons[0].object_id == "controller_1" + assert len(c2_buttons) == 1 and c2_buttons[0].object_id == "controller_2" assert ( - len(main_selects) == 1 - and main_selects[0].object_id == "duplicate-entities-test" + len(main_buttons) == 1 + and main_buttons[0].object_id == "duplicate-entities-test" ) - # Scenario 8: Check special characters in number names - now unique - temp_numbers = [n for n in numbers if "Temperature Setpoint!" in n.name] + # Scenario 7: Check special characters in number names + temp_numbers = [n for n in numbers if n.name == "Temperature Setpoint!"] assert len(temp_numbers) == 2, ( f"Expected exactly 2 temperature setpoint numbers, got {len(temp_numbers)}" ) - # Should have unique names - setpoint_names = set() + # Special characters should be sanitized to _ in object_id for number in temp_numbers: - setpoint_names.add(number.name) - - assert len(setpoint_names) == 2, ( - f"Temperature setpoint numbers should have unique names, got {setpoint_names}" - ) + assert number.object_id == "temperature_setpoint_", ( + f"Expected object_id 'temperature_setpoint_', got '{number.object_id}'" + ) # Verify we can get states for all entities (ensures they're functional) loop = asyncio.get_running_loop() @@ -192,7 +164,6 @@ async def test_duplicate_entities_not_allowed_on_different_devices( + len(switches) + len(buttons) + len(numbers) - + len(selects) ) def on_state(state) -> None: @@ -206,7 +177,7 @@ async def test_duplicate_entities_not_allowed_on_different_devices( # Wait for all entity states try: await asyncio.wait_for(states_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Did not receive all entity states within 10 seconds. " f"Expected {expected_count}, received {state_count}" diff --git a/tests/integration/test_entity_icon.py b/tests/integration/test_entity_icon.py index aec7168165..a634ae385e 100644 --- a/tests/integration/test_entity_icon.py +++ b/tests/integration/test_entity_icon.py @@ -82,7 +82,7 @@ async def test_entity_icon( # Wait for states try: await asyncio.wait_for(state_received.wait(), timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("No states received within 5 seconds") # Verify we received states diff --git a/tests/integration/test_host_mode_api_password.py b/tests/integration/test_host_mode_api_password.py new file mode 100644 index 0000000000..825c2c55f2 --- /dev/null +++ b/tests/integration/test_host_mode_api_password.py @@ -0,0 +1,53 @@ +"""Integration test for API password authentication.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import APIConnectionError +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_host_mode_api_password( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test API authentication with password.""" + async with run_compiled(yaml_config): + # Connect with correct password + async with api_client_connected(password="test_password_123") as client: + # Verify we can get device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.uses_password is True + assert device_info.name == "host-mode-api-password" + + # Subscribe to states to ensure authenticated connection works + loop = asyncio.get_running_loop() + state_future: asyncio.Future[bool] = loop.create_future() + states = {} + + def on_state(state): + states[state.key] = state + if not state_future.done(): + state_future.set_result(True) + + client.subscribe_states(on_state) + + # Wait for at least one state with timeout + try: + await asyncio.wait_for(state_future, timeout=5.0) + except TimeoutError: + pytest.fail("No states received within timeout") + + # Should have received at least one state (the test sensor) + 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 diff --git a/tests/integration/test_host_mode_batch_delay.py b/tests/integration/test_host_mode_batch_delay.py index 5165b90e47..a3f666fa21 100644 --- a/tests/integration/test_host_mode_batch_delay.py +++ b/tests/integration/test_host_mode_batch_delay.py @@ -44,7 +44,7 @@ async def test_host_mode_batch_delay( # Wait for states from all entities with timeout try: entity_count = await asyncio.wait_for(entity_count_future, timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Did not receive states from at least 7 entities within 5 seconds. " f"Received {len(states)} states" diff --git a/tests/integration/test_host_mode_empty_string_options.py b/tests/integration/test_host_mode_empty_string_options.py index 16399dcfb8..242db2d40f 100644 --- a/tests/integration/test_host_mode_empty_string_options.py +++ b/tests/integration/test_host_mode_empty_string_options.py @@ -99,7 +99,7 @@ async def test_host_mode_empty_string_options( # Wait for initial states with timeout try: await asyncio.wait_for(states_received_future, timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Did not receive states for all select entities. " f"Expected keys: {expected_select_keys}, Received: {received_select_keys}" diff --git a/tests/integration/test_host_mode_entity_fields.py b/tests/integration/test_host_mode_entity_fields.py index b9fa3e9746..5ec1b64a99 100644 --- a/tests/integration/test_host_mode_entity_fields.py +++ b/tests/integration/test_host_mode_entity_fields.py @@ -86,7 +86,7 @@ async def test_host_mode_entity_fields( # Wait for at least one state try: await asyncio.wait_for(state_received.wait(), timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("No states received within 5 seconds") # Verify we received states (which means has_state flag is working) diff --git a/tests/integration/test_host_mode_many_entities.py b/tests/integration/test_host_mode_many_entities.py index 19d1ee315f..ce9e157a88 100644 --- a/tests/integration/test_host_mode_many_entities.py +++ b/tests/integration/test_host_mode_many_entities.py @@ -41,7 +41,7 @@ async def test_host_mode_many_entities( # Wait for states from at least 50 sensors with timeout try: sensor_count = await asyncio.wait_for(sensor_count_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: sensor_states = [ s for s in states.values() diff --git a/tests/integration/test_host_mode_many_entities_multiple_connections.py b/tests/integration/test_host_mode_many_entities_multiple_connections.py index a4e5f8a45c..a7939bb277 100644 --- a/tests/integration/test_host_mode_many_entities_multiple_connections.py +++ b/tests/integration/test_host_mode_many_entities_multiple_connections.py @@ -50,7 +50,7 @@ async def test_host_mode_many_entities_multiple_connections( asyncio.wait_for(client1_ready, timeout=10.0), asyncio.wait_for(client2_ready, timeout=10.0), ) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"One or both clients did not receive enough states within 10 seconds. " f"Client1: {len(states1)}, Client2: {len(states2)}" diff --git a/tests/integration/test_host_mode_sensor.py b/tests/integration/test_host_mode_sensor.py index 8c1e9f5d51..e28d3419e6 100644 --- a/tests/integration/test_host_mode_sensor.py +++ b/tests/integration/test_host_mode_sensor.py @@ -40,7 +40,7 @@ async def test_host_mode_with_sensor( # Wait for sensor with specific value (42.0) with timeout try: test_sensor_state = await asyncio.wait_for(sensor_future, timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Sensor with value 42.0 not received within 5 seconds. " f"Received states: {list(states.values())}" diff --git a/tests/integration/test_loop_disable_enable.py b/tests/integration/test_loop_disable_enable.py index e93fc32178..2a866b1574 100644 --- a/tests/integration/test_loop_disable_enable.py +++ b/tests/integration/test_loop_disable_enable.py @@ -150,7 +150,7 @@ async def test_loop_disable_enable( # Wait for self_disable_10 to disable itself try: await asyncio.wait_for(self_disable_10_disabled.wait(), timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("self_disable_10 did not disable itself within 10 seconds") # Verify it ran at least 10 times before disabling @@ -164,7 +164,7 @@ async def test_loop_disable_enable( # Wait for normal_component to run at least 10 times try: await asyncio.wait_for(normal_component_10_loops.wait(), timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"normal_component did not reach 10 loops within timeout, got {len(normal_component_counts)}" ) @@ -172,12 +172,12 @@ async def test_loop_disable_enable( # Wait for redundant operation tests try: await asyncio.wait_for(redundant_enable_tested.wait(), timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("redundant_enable did not test enabling when already enabled") try: await asyncio.wait_for(redundant_disable_tested.wait(), timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( "redundant_disable did not test disabling when will be disabled" ) @@ -185,7 +185,7 @@ async def test_loop_disable_enable( # Wait to see if self_disable_10 gets re-enabled try: await asyncio.wait_for(self_disable_10_re_enabled.wait(), timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("self_disable_10 was not re-enabled within 5 seconds") # Component was re-enabled - verify it ran more times @@ -198,7 +198,7 @@ async def test_loop_disable_enable( # Wait for ISR component to disable itself after 5 loops try: await asyncio.wait_for(isr_component_disabled.wait(), timeout=3.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("ISR component did not disable itself within 3 seconds") # Verify it ran exactly 5 times before disabling @@ -210,7 +210,7 @@ async def test_loop_disable_enable( # Wait for component to be re-enabled by periodic ISR simulation and run again try: await asyncio.wait_for(isr_component_re_enabled.wait(), timeout=2.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("ISR component was not re-enabled after ISR call") # Verify it's running again after ISR enable @@ -222,7 +222,7 @@ async def test_loop_disable_enable( # Wait for pure ISR enable (no main loop enable) to work try: await asyncio.wait_for(isr_component_pure_re_enabled.wait(), timeout=2.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("ISR component was not re-enabled by pure ISR call") # Verify it ran after pure ISR enable @@ -235,7 +235,7 @@ async def test_loop_disable_enable( # Wait for update component to disable its loop try: await asyncio.wait_for(update_component_loop_disabled.wait(), timeout=3.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Update component did not disable its loop within 3 seconds") # Verify it ran exactly 3 loops before disabling @@ -248,7 +248,7 @@ async def test_loop_disable_enable( await asyncio.wait_for( update_component_manual_update_called.wait(), timeout=5.0 ) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Manual component.update was not called within 5 seconds") # The key test: verify that manual component.update worked after loop was disabled diff --git a/tests/integration/test_runtime_stats.py b/tests/integration/test_runtime_stats.py new file mode 100644 index 0000000000..9e93035d83 --- /dev/null +++ b/tests/integration/test_runtime_stats.py @@ -0,0 +1,88 @@ +"""Test runtime statistics component.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_runtime_stats( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test runtime stats logs statistics at configured interval and tracks components.""" + loop = asyncio.get_running_loop() + + # Track how many times we see the total stats + stats_count = 0 + first_stats_future = loop.create_future() + second_stats_future = loop.create_future() + + # Track component stats + component_stats_found = set() + + # Patterns to match - need to handle ANSI color codes and timestamps + # The log format is: [HH:MM:SS][color codes][I][tag]: message + total_stats_pattern = re.compile(r"Total stats \(since boot\):") + # Match component names that may include dots (e.g., template.sensor) + component_pattern = re.compile( + r"^\[[^\]]+\].*?\s+([\w.]+):\s+count=(\d+),\s+avg=([\d.]+)ms" + ) + + def check_output(line: str) -> None: + """Check log output for runtime stats messages.""" + nonlocal stats_count + + # Check for total stats line + if total_stats_pattern.search(line): + stats_count += 1 + + if stats_count == 1 and not first_stats_future.done(): + first_stats_future.set_result(True) + elif stats_count == 2 and not second_stats_future.done(): + second_stats_future.set_result(True) + + # Check for component stats + match = component_pattern.match(line) + if match: + component_name = match.group(1) + component_stats_found.add(component_name) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device is connected + device_info = await client.device_info() + assert device_info is not None + + # Wait for first "Total stats" log (should happen at 1s) + try: + await asyncio.wait_for(first_stats_future, timeout=5.0) + except TimeoutError: + pytest.fail("First 'Total stats' log not seen within 5 seconds") + + # Wait for second "Total stats" log (should happen at 2s) + try: + await asyncio.wait_for(second_stats_future, timeout=5.0) + except TimeoutError: + pytest.fail(f"Second 'Total stats' log not seen. Total seen: {stats_count}") + + # Verify we got at least 2 stats logs + assert stats_count >= 2, ( + f"Expected at least 2 'Total stats' logs, got {stats_count}" + ) + + # Verify we found stats for our components + assert "template.sensor" in component_stats_found, ( + f"Expected template.sensor stats, found: {component_stats_found}" + ) + assert "template.switch" in component_stats_found, ( + f"Expected template.switch stats, found: {component_stats_found}" + ) diff --git a/tests/integration/test_scheduler_bulk_cleanup.py b/tests/integration/test_scheduler_bulk_cleanup.py index 08ff293b84..b52a4a3496 100644 --- a/tests/integration/test_scheduler_bulk_cleanup.py +++ b/tests/integration/test_scheduler_bulk_cleanup.py @@ -103,7 +103,7 @@ async def test_scheduler_bulk_cleanup( # Wait for test completion try: await asyncio.wait_for(test_complete_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Bulk cleanup test timed out") # Verify bulk cleanup was triggered diff --git a/tests/integration/test_scheduler_defer_cancel.py b/tests/integration/test_scheduler_defer_cancel.py index 923cf946c4..7bce0eda54 100644 --- a/tests/integration/test_scheduler_defer_cancel.py +++ b/tests/integration/test_scheduler_defer_cancel.py @@ -85,7 +85,7 @@ async def test_scheduler_defer_cancel( try: await asyncio.wait_for(test_complete_future, timeout=10.0) executed_defer = await asyncio.wait_for(test_result_future, timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Test did not complete within timeout") # Verify that only defer 10 was executed diff --git a/tests/integration/test_scheduler_defer_cancel_regular.py b/tests/integration/test_scheduler_defer_cancel_regular.py index 57b7134feb..c93d814fbe 100644 --- a/tests/integration/test_scheduler_defer_cancel_regular.py +++ b/tests/integration/test_scheduler_defer_cancel_regular.py @@ -64,7 +64,7 @@ async def test_scheduler_defer_cancels_regular( # Wait for test completion try: await asyncio.wait_for(test_complete_future, timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail(f"Test timed out. Log messages: {log_messages}") # Verify results diff --git a/tests/integration/test_scheduler_defer_fifo_simple.py b/tests/integration/test_scheduler_defer_fifo_simple.py index eb4058fedd..3502302368 100644 --- a/tests/integration/test_scheduler_defer_fifo_simple.py +++ b/tests/integration/test_scheduler_defer_fifo_simple.py @@ -90,7 +90,7 @@ async def test_scheduler_defer_fifo_simple( try: await asyncio.wait_for(test_complete_future, timeout=5.0) test1_passed = await asyncio.wait_for(test_result_future, timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Test set_timeout(0) did not complete within 5 seconds") assert test1_passed is True, ( @@ -108,7 +108,7 @@ async def test_scheduler_defer_fifo_simple( try: await asyncio.wait_for(test_complete_future, timeout=5.0) test2_passed = await asyncio.wait_for(test_result_future, timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Test defer() did not complete within 5 seconds") # Verify the test passed diff --git a/tests/integration/test_scheduler_defer_stress.py b/tests/integration/test_scheduler_defer_stress.py index d546b7132f..6f4d997307 100644 --- a/tests/integration/test_scheduler_defer_stress.py +++ b/tests/integration/test_scheduler_defer_stress.py @@ -97,7 +97,7 @@ async def test_scheduler_defer_stress( # Wait for all defers to execute (should be quick) try: await asyncio.wait_for(test_complete_future, timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: # Report how many we got pytest.fail( f"Stress test timed out. Only {len(executed_defers)} of " diff --git a/tests/integration/test_scheduler_heap_stress.py b/tests/integration/test_scheduler_heap_stress.py index 3c757bfc9d..2d55b8ae89 100644 --- a/tests/integration/test_scheduler_heap_stress.py +++ b/tests/integration/test_scheduler_heap_stress.py @@ -103,13 +103,14 @@ async def test_scheduler_heap_stress( # Wait for all callbacks to execute (should be quick, but give more time for scheduling) try: - await asyncio.wait_for(test_complete_future, timeout=60.0) - except asyncio.TimeoutError: + await asyncio.wait_for(test_complete_future, timeout=10.0) + except TimeoutError: # Report how many we got + missing_ids = sorted(set(range(1000)) - executed_callbacks) pytest.fail( f"Stress test timed out. Only {len(executed_callbacks)} of " f"1000 callbacks executed. Missing IDs: " - f"{sorted(set(range(1000)) - executed_callbacks)[:10]}..." + f"{missing_ids[:20]}... (total missing: {len(missing_ids)})" ) # Verify all callbacks executed diff --git a/tests/integration/test_scheduler_null_name.py b/tests/integration/test_scheduler_null_name.py index 41bcd8aed7..66e25d4a11 100644 --- a/tests/integration/test_scheduler_null_name.py +++ b/tests/integration/test_scheduler_null_name.py @@ -53,7 +53,7 @@ async def test_scheduler_null_name( # Wait for test completion try: await asyncio.wait_for(test_complete_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( "Test did not complete within timeout - likely crashed due to NULL name" ) diff --git a/tests/integration/test_scheduler_rapid_cancellation.py b/tests/integration/test_scheduler_rapid_cancellation.py index 90577f36f1..6b6277c752 100644 --- a/tests/integration/test_scheduler_rapid_cancellation.py +++ b/tests/integration/test_scheduler_rapid_cancellation.py @@ -112,7 +112,7 @@ async def test_scheduler_rapid_cancellation( # Wait for test to complete with timeout try: await asyncio.wait_for(test_complete_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail(f"Test timed out. Stats: {test_stats}") # Check for any errors diff --git a/tests/integration/test_scheduler_recursive_timeout.py b/tests/integration/test_scheduler_recursive_timeout.py index c015978e15..d98d2ac5ee 100644 --- a/tests/integration/test_scheduler_recursive_timeout.py +++ b/tests/integration/test_scheduler_recursive_timeout.py @@ -84,7 +84,7 @@ async def test_scheduler_recursive_timeout( # Wait for test to complete try: await asyncio.wait_for(test_complete_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"Recursive timeout test timed out. Got sequence: {execution_sequence}" ) diff --git a/tests/integration/test_scheduler_retry_test.py b/tests/integration/test_scheduler_retry_test.py new file mode 100644 index 0000000000..0c4d573c1b --- /dev/null +++ b/tests/integration/test_scheduler_retry_test.py @@ -0,0 +1,234 @@ +"""Test scheduler retry functionality.""" + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_retry_test( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that scheduler retry functionality works correctly.""" + # Track test progress + simple_retry_done = asyncio.Event() + backoff_retry_done = asyncio.Event() + immediate_done_done = asyncio.Event() + cancel_retry_done = asyncio.Event() + empty_name_retry_done = asyncio.Event() + component_retry_done = asyncio.Event() + multiple_name_done = asyncio.Event() + test_complete = asyncio.Event() + + # Track retry counts + simple_retry_count = 0 + backoff_retry_count = 0 + immediate_done_count = 0 + cancel_retry_count = 0 + empty_name_retry_count = 0 + component_retry_count = 0 + multiple_name_count = 0 + + # Track specific test results + cancel_result = None + empty_cancel_result = None + backoff_intervals = [] + + def on_log_line(line: str) -> None: + nonlocal simple_retry_count, backoff_retry_count, immediate_done_count + nonlocal cancel_retry_count, empty_name_retry_count, component_retry_count + nonlocal multiple_name_count, cancel_result, empty_cancel_result + + # Strip ANSI color codes + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + + # Simple retry test + if "Simple retry attempt" in clean_line: + if match := re.search(r"Simple retry attempt (\d+)", clean_line): + simple_retry_count = int(match.group(1)) + + elif "Simple retry succeeded on attempt" in clean_line: + simple_retry_done.set() + + # Backoff retry test + elif "Backoff retry attempt" in clean_line: + if match := re.search( + r"Backoff retry attempt (\d+).*interval=(\d+)ms", clean_line + ): + backoff_retry_count = int(match.group(1)) + interval = int(match.group(2)) + if backoff_retry_count > 1: # Skip first (immediate) call + backoff_intervals.append(interval) + + elif "Backoff retry completed" in clean_line: + backoff_retry_done.set() + + # Immediate done test + elif "Immediate done retry called" in clean_line: + immediate_done_count += 1 + immediate_done_done.set() + + # Cancel retry test + elif "Cancel test retry attempt" in clean_line: + cancel_retry_count += 1 + + elif "Retry cancellation result:" in clean_line: + cancel_result = "true" in clean_line + cancel_retry_done.set() + + # Empty name retry test + elif "Empty name retry attempt" in clean_line: + if match := re.search(r"Empty name retry attempt (\d+)", clean_line): + empty_name_retry_count = int(match.group(1)) + + elif "Empty name retry cancel result:" in clean_line: + empty_cancel_result = "true" in clean_line + + elif "Empty name retry ran" in clean_line: + empty_name_retry_done.set() + + # Component retry test + elif "Component retry attempt" in clean_line: + if match := re.search(r"Component retry attempt (\d+)", clean_line): + component_retry_count = int(match.group(1)) + if component_retry_count >= 2: + component_retry_done.set() + + # Multiple same name test + elif "Second duplicate retry attempt" in clean_line: + if match := re.search(r"counter=(\d+)", clean_line): + multiple_name_count = int(match.group(1)) + if multiple_name_count >= 20: + multiple_name_done.set() + + # Test completion + elif "All retry tests completed" in clean_line: + test_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + 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-retry-test" + + # Wait for simple retry test + try: + await asyncio.wait_for(simple_retry_done.wait(), timeout=1.0) + except TimeoutError: + pytest.fail( + f"Simple retry test did not complete. Count: {simple_retry_count}" + ) + + assert simple_retry_count == 2, ( + f"Expected 2 simple retry attempts, got {simple_retry_count}" + ) + + # Wait for backoff retry test + try: + await asyncio.wait_for(backoff_retry_done.wait(), timeout=3.0) + except TimeoutError: + pytest.fail( + f"Backoff retry test did not complete. Count: {backoff_retry_count}" + ) + + assert backoff_retry_count == 4, ( + f"Expected 4 backoff retry attempts, got {backoff_retry_count}" + ) + + # Verify backoff intervals (allowing for timing variations) + assert len(backoff_intervals) >= 2, ( + f"Expected at least 2 intervals, got {len(backoff_intervals)}" + ) + if len(backoff_intervals) >= 3: + # First interval should be ~50ms + assert 30 <= backoff_intervals[0] <= 70, ( + f"First interval {backoff_intervals[0]}ms not ~50ms" + ) + # Second interval should be ~100ms (50ms * 2.0) + assert 80 <= backoff_intervals[1] <= 120, ( + f"Second interval {backoff_intervals[1]}ms not ~100ms" + ) + # Third interval should be ~200ms (100ms * 2.0) + assert 180 <= backoff_intervals[2] <= 220, ( + f"Third interval {backoff_intervals[2]}ms not ~200ms" + ) + + # Wait for immediate done test + try: + await asyncio.wait_for(immediate_done_done.wait(), timeout=3.0) + except TimeoutError: + pytest.fail( + f"Immediate done test did not complete. Count: {immediate_done_count}" + ) + + assert immediate_done_count == 1, ( + f"Expected 1 immediate done call, got {immediate_done_count}" + ) + + # Wait for cancel retry test + try: + await asyncio.wait_for(cancel_retry_done.wait(), timeout=2.0) + except TimeoutError: + pytest.fail( + f"Cancel retry test did not complete. Count: {cancel_retry_count}" + ) + + assert cancel_result is True, "Retry cancellation should have succeeded" + assert 2 <= cancel_retry_count <= 5, ( + f"Expected 2-5 cancel retry attempts before cancellation, got {cancel_retry_count}" + ) + + # Wait for empty name retry test + try: + await asyncio.wait_for(empty_name_retry_done.wait(), timeout=1.0) + except TimeoutError: + pytest.fail( + f"Empty name retry test did not complete. Count: {empty_name_retry_count}" + ) + + # Empty name retry should run at least once before being cancelled + assert 1 <= empty_name_retry_count <= 2, ( + f"Expected 1-2 empty name retry attempts, got {empty_name_retry_count}" + ) + assert empty_cancel_result is True, ( + "Empty name retry cancel should have succeeded" + ) + + # Wait for component retry test + try: + await asyncio.wait_for(component_retry_done.wait(), timeout=1.0) + except TimeoutError: + pytest.fail( + f"Component retry test did not complete. Count: {component_retry_count}" + ) + + assert component_retry_count >= 2, ( + f"Expected at least 2 component retry attempts, got {component_retry_count}" + ) + + # Wait for multiple same name test + try: + await asyncio.wait_for(multiple_name_done.wait(), timeout=1.0) + except TimeoutError: + pytest.fail( + f"Multiple same name test did not complete. Count: {multiple_name_count}" + ) + + # Should be 20+ (only second retry should run) + assert multiple_name_count >= 20, ( + f"Expected multiple name count >= 20 (second retry only), got {multiple_name_count}" + ) + + # Wait for test completion + try: + await asyncio.wait_for(test_complete.wait(), timeout=1.0) + except TimeoutError: + pytest.fail("Test did not complete within timeout") diff --git a/tests/integration/test_scheduler_simultaneous_callbacks.py b/tests/integration/test_scheduler_simultaneous_callbacks.py index f5120ce4ce..82fd0fc01e 100644 --- a/tests/integration/test_scheduler_simultaneous_callbacks.py +++ b/tests/integration/test_scheduler_simultaneous_callbacks.py @@ -103,7 +103,7 @@ async def test_scheduler_simultaneous_callbacks( # Wait for test to complete try: await asyncio.wait_for(test_complete_future, timeout=30.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail(f"Simultaneous callbacks test timed out. Stats: {test_stats}") # Check for any errors diff --git a/tests/integration/test_scheduler_string_lifetime.py b/tests/integration/test_scheduler_string_lifetime.py index 4d77abd954..7ec5a54373 100644 --- a/tests/integration/test_scheduler_string_lifetime.py +++ b/tests/integration/test_scheduler_string_lifetime.py @@ -157,7 +157,7 @@ async def test_scheduler_string_lifetime( client.execute_service(test_services["final"], {}) await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail(f"String lifetime test timed out. Stats: {test_stats}") # Check for any errors diff --git a/tests/integration/test_scheduler_string_name_stress.py b/tests/integration/test_scheduler_string_name_stress.py index 3045842223..4c52913e63 100644 --- a/tests/integration/test_scheduler_string_name_stress.py +++ b/tests/integration/test_scheduler_string_name_stress.py @@ -97,7 +97,7 @@ async def test_scheduler_string_name_stress( # Wait for test to complete or crash try: await asyncio.wait_for(test_complete_future, timeout=30.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"String name stress test timed out. Executed {len(executed_callbacks)} callbacks. " f"This might indicate a deadlock." diff --git a/tests/integration/test_scheduler_string_test.py b/tests/integration/test_scheduler_string_test.py index f3a36b2db7..783ed37c13 100644 --- a/tests/integration/test_scheduler_string_test.py +++ b/tests/integration/test_scheduler_string_test.py @@ -122,22 +122,22 @@ async def test_scheduler_string_test( # Wait for static string tests try: await asyncio.wait_for(static_timeout_1_fired.wait(), timeout=0.5) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Static timeout 1 did not fire within 0.5 seconds") try: await asyncio.wait_for(static_timeout_2_fired.wait(), timeout=0.5) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Static timeout 2 did not fire within 0.5 seconds") try: await asyncio.wait_for(static_interval_fired.wait(), timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Static interval did not fire within 1 second") try: await asyncio.wait_for(static_interval_cancelled.wait(), timeout=2.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Static interval was not cancelled within 2 seconds") # Verify static interval ran at least 3 times @@ -153,41 +153,41 @@ async def test_scheduler_string_test( # Wait for static defer tests try: await asyncio.wait_for(static_defer_1_fired.wait(), timeout=0.5) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Static defer 1 did not fire within 0.5 seconds") try: await asyncio.wait_for(static_defer_2_fired.wait(), timeout=0.5) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Static defer 2 did not fire within 0.5 seconds") # Wait for dynamic string tests try: await asyncio.wait_for(dynamic_timeout_fired.wait(), timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Dynamic timeout did not fire within 1 second") try: await asyncio.wait_for(dynamic_interval_fired.wait(), timeout=1.5) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Dynamic interval did not fire within 1.5 seconds") # Wait for dynamic defer test try: await asyncio.wait_for(dynamic_defer_fired.wait(), timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Dynamic defer did not fire within 1 second") # Wait for cancel test try: await asyncio.wait_for(cancel_test_done.wait(), timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Cancel test did not complete within 1 second") # Wait for final results try: await asyncio.wait_for(final_results_logged.wait(), timeout=4.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Final results were not logged within 4 seconds") # Verify results diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index d0db08e6f7..423e2d3c30 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -986,8 +986,7 @@ def test_get_components_from_integration_fixtures() -> None: with ( patch("pathlib.Path.glob") as mock_glob, - patch("builtins.open", create=True), - patch("yaml.safe_load", return_value=yaml_content), + patch("esphome.yaml_util.load_yaml", return_value=yaml_content), ): mock_glob.return_value = [mock_yaml_file] diff --git a/tests/test_build_components/build_components_base.nrf52-adafruit.yaml b/tests/test_build_components/build_components_base.nrf52-adafruit.yaml new file mode 100644 index 0000000000..05e3a6387c --- /dev/null +++ b/tests/test_build_components/build_components_base.nrf52-adafruit.yaml @@ -0,0 +1,16 @@ +esphome: + name: componenttestnrf52 + friendly_name: $component_name + +nrf52: + board: adafruit_itsybitsy_nrf52840 + bootloader: adafruit_nrf52_sd140_v6 + +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/build_components_base.nrf52-mcumgr.yaml b/tests/test_build_components/build_components_base.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..04211ffdfe --- /dev/null +++ b/tests/test_build_components/build_components_base.nrf52-mcumgr.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestnrf52 + friendly_name: $component_name + +nrf52: + board: adafruit_feather_nrf52840 + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 4f256ffb33..c639ad94b2 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -510,13 +510,13 @@ def test_entity_duplicate_validator() -> None: config1 = {CONF_NAME: "Temperature"} validated1 = validator(config1) assert validated1 == config1 - assert ("sensor", "temperature") in CORE.unique_ids + assert ("", "sensor", "temperature") in CORE.unique_ids # Second entity with different name should pass config2 = {CONF_NAME: "Humidity"} validated2 = validator(config2) assert validated2 == config2 - assert ("sensor", "humidity") in CORE.unique_ids + assert ("", "sensor", "humidity") in CORE.unique_ids # Duplicate entity should fail config3 = {CONF_NAME: "Temperature"} @@ -535,36 +535,24 @@ def test_entity_duplicate_validator_with_devices() -> None: device1 = ID("device1", type="Device") device2 = ID("device2", type="Device") - # First entity on device1 should pass + # Same name on different devices should pass config1 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} validated1 = validator(config1) assert validated1 == config1 - assert ("sensor", "temperature") in CORE.unique_ids + assert ("device1", "sensor", "temperature") in CORE.unique_ids - # Same name on different device should now fail config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device2} + validated2 = validator(config2) + assert validated2 == config2 + assert ("device2", "sensor", "temperature") in CORE.unique_ids + + # Duplicate on same device should fail + config3 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} with pytest.raises( Invalid, - match=r"Duplicate sensor entity with name 'Temperature' found. Each entity must have a unique name within its platform across all devices.", + match=r"Duplicate sensor entity with name 'Temperature' found on device 'device1'", ): - validator(config2) - - # Different name on device2 should pass - config3 = {CONF_NAME: "Humidity", CONF_DEVICE_ID: device2} - validated3 = validator(config3) - assert validated3 == config3 - assert ("sensor", "humidity") in CORE.unique_ids - - # Empty names should use device names and be allowed - config4 = {CONF_NAME: "", CONF_DEVICE_ID: device1} - validated4 = validator(config4) - assert validated4 == config4 - assert ("sensor", "device1") in CORE.unique_ids - - config5 = {CONF_NAME: "", CONF_DEVICE_ID: device2} - validated5 = validator(config5) - assert validated5 == config5 - assert ("sensor", "device2") in CORE.unique_ids + validator(config3) def test_duplicate_entity_yaml_validation( @@ -588,10 +576,10 @@ def test_duplicate_entity_with_devices_yaml_validation( ) assert result is None - # Check for the duplicate entity error message + # Check for the duplicate entity error message with device captured = capsys.readouterr() assert ( - "Duplicate sensor entity with name 'Temperature' found. Each entity must have a unique name within its platform across all devices." + "Duplicate sensor entity with name 'Temperature' found on device 'device1'" in captured.out ) @@ -616,21 +604,22 @@ def test_entity_duplicate_validator_internal_entities() -> None: config1 = {CONF_NAME: "Temperature"} validated1 = validator(config1) assert validated1 == config1 - assert ("sensor", "temperature") in CORE.unique_ids + # New format includes device_id (empty string for main device) + assert ("", "sensor", "temperature") in CORE.unique_ids # Internal entity with same name should pass (not added to unique_ids) config2 = {CONF_NAME: "Temperature", CONF_INTERNAL: True} 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 + assert len([k for k in CORE.unique_ids if k == ("", "sensor", "temperature")]) == 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 + assert len([k for k in CORE.unique_ids if k == ("", "sensor", "temperature")]) == 1 # Non-internal entity with same name should fail config4 = {CONF_NAME: "Temperature"}