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 new file mode 100644 index 0000000000..50a7fa9709 --- /dev/null +++ b/.clang-tidy.hash @@ -0,0 +1 @@ +0c2acbc16bfb7d63571dbe7042f94f683be25e4ca8a0f158a960a94adac4b931 diff --git a/.coveragerc b/.coveragerc index 12e48ec395..f23592be24 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,4 @@ [run] -omit = +omit = esphome/components/* tests/integration/* diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..44722ec85c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,92 @@ +name: Report an issue with ESPHome +description: Report an issue with ESPHome. +body: + - type: markdown + attributes: + value: | + This issue form is for reporting bugs only! + + If you have a feature request or enhancement, please [request them here instead][fr]. + + [fr]: https://github.com/orgs/esphome/discussions + - type: textarea + validations: + required: true + id: problem + attributes: + label: The problem + description: >- + Describe the issue you are experiencing here to communicate to the + maintainers. Tell us what you were trying to do and what happened. + + Provide a clear and concise description of what the problem is. + + - type: markdown + attributes: + value: | + ## Environment + - type: input + id: version + validations: + required: true + attributes: + label: Which version of ESPHome has the issue? + description: > + ESPHome version like 1.19, 2025.6.0 or 2025.XX.X-dev. + - type: dropdown + validations: + required: true + id: installation + attributes: + label: What type of installation are you using? + options: + - Home Assistant Add-on + - Docker + - pip + - type: dropdown + validations: + required: true + id: platform + attributes: + label: What platform are you using? + options: + - ESP8266 + - ESP32 + - RP2040 + - BK72XX + - RTL87XX + - LN882X + - Host + - Other + - type: input + id: component_name + attributes: + label: Component causing the issue + description: > + The name of the component or platform. For example, api/i2c or ultrasonic. + + - type: markdown + attributes: + value: | + # Details + - type: textarea + id: config + attributes: + label: YAML Config + description: | + Include a complete YAML configuration file demonstrating the problem here. Preferably post the *entire* file - don't make assumptions about what is unimportant. However, if it's a large or complicated config then you will need to reduce it to the smallest possible file *that still demonstrates the problem*. If you don't provide enough information to *easily* reproduce the problem, it's unlikely your bug report will get any attention. Logs do not belong here, attach them below. + render: yaml + - type: textarea + id: logs + attributes: + label: Anything in the logs that might be useful for us? + description: For example, error message, or stack traces. Serial or USB logs are much more useful than WiFi logs. + render: txt + - type: textarea + id: additional + attributes: + label: Additional information + description: > + If you have any additional information for us, use the field below. + Please note, you can attach screenshots or screen recordings here, by + dragging and dropping files in the field below. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 804dad47c7..19f52349a6 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,15 +1,21 @@ --- blank_issues_enabled: false contact_links: - - name: Issue Tracker - url: https://github.com/esphome/issues - about: Please create bug reports in the dedicated issue tracker. - - name: Feature Request Tracker - url: https://github.com/esphome/feature-requests - about: | - Please create feature requests in the dedicated feature request tracker. + - name: Report an issue with the ESPHome documentation + url: https://github.com/esphome/esphome-docs/issues/new/choose + about: Report an issue with the ESPHome documentation. + - name: Report an issue with the ESPHome web server + url: https://github.com/esphome/esphome-webserver/issues/new/choose + about: Report an issue with the ESPHome web server. + - name: Report an issue with the ESPHome Builder / Dashboard + url: https://github.com/esphome/dashboard/issues/new/choose + about: Report an issue with the ESPHome Builder / Dashboard. + - name: Report an issue with the ESPHome API client + url: https://github.com/esphome/aioesphomeapi/issues/new/choose + about: Report an issue with the ESPHome API client. + - name: Make a Feature Request + url: https://github.com/orgs/esphome/discussions + about: Please create feature requests in the dedicated feature request tracker. - name: Frequently Asked Question url: https://esphome.io/guides/faq.html - about: | - Please view the FAQ for common questions and what - to include in a bug report. + about: Please view the FAQ for common questions and what to include in a bug report. 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/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 082539adaa..3a7b301b60 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -41,7 +41,7 @@ runs: shell: bash run: | python -m venv venv - ./venv/Scripts/activate + source ./venv/Scripts/activate python --version pip install -r requirements.txt -r requirements_test.txt pip install -e . 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..b30f6cf28a --- /dev/null +++ b/.github/workflows/auto-label-pr.yml @@ -0,0 +1,517 @@ +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: + SMALL_PR_THRESHOLD: 30 + MAX_LABELS: 15 + TOO_BIG_THRESHOLD: 1000 + +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; + + // Hidden marker to identify bot comments from this workflow + const BOT_COMMENT_MARKER = ''; + + // 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(); + + // Fetch TARGET_PLATFORMS and PLATFORM_COMPONENTS from API + let targetPlatforms = []; + let platformComponents = []; + + try { + const response = await fetch('https://data.esphome.io/components.json'); + const componentsData = await response.json(); + + // Extract target platforms and platform components directly from API + targetPlatforms = componentsData.target_platforms || []; + platformComponents = componentsData.platform_components || []; + + console.log('Target platforms from API:', targetPlatforms.length, targetPlatforms); + console.log('Platform components from API:', platformComponents.length, platformComponents); + } catch (error) { + console.log('Failed to fetch components data from API:', error.message); + } + + // Get environment variables + const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); + const maxLabels = parseInt('${{ env.MAX_LABELS }}'); + const tooBigThreshold = parseInt('${{ env.TOO_BIG_THRESHOLD }}'); + + // 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); + + // Calculate changes excluding root tests directory for too-big calculation + const testChanges = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); + + const nonTestChanges = totalChanges - testChanges; + console.log(`Test changes: ${testChanges}, Non-test changes: ${nonTestChanges}`); + + // 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(', ')); + + // Check if PR has mega-pr label + const isMegaPR = currentLabels.includes('mega-pr'); + + // Check if PR is too big (either too many labels or too many line changes) + const tooManyLabels = finalLabels.length > maxLabels; + const tooManyChanges = nonTestChanges > tooBigThreshold; + + if ((tooManyLabels || tooManyChanges) && !isMegaPR) { + const originalLength = finalLabels.length; + console.log(`PR is too big - Labels: ${originalLength}, Changes: ${totalChanges} (non-test: ${nonTestChanges})`); + + // Get all reviews on this PR to check for existing bot reviews + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: pr_number + }); + + // Check if there's already an active bot review requesting changes + const existingBotReview = reviews.find(review => + review.user.type === 'Bot' && + review.state === 'CHANGES_REQUESTED' && + review.body && review.body.includes(BOT_COMMENT_MARKER) + ); + + // If too big due to line changes only, keep original labels and add too-big + // If too big due to too many labels, replace with just too-big + if (tooManyChanges && !tooManyLabels) { + finalLabels.push('too-big'); + } else { + finalLabels = ['too-big']; + } + + // Only create a new review if there isn't already an active bot review + if (!existingBotReview) { + // Create appropriate review message + let reviewBody; + if (tooManyLabels && tooManyChanges) { + reviewBody = `${BOT_COMMENT_MARKER}\nThis PR is too large with ${nonTestChanges} line changes (excluding tests) 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.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; + } else if (tooManyLabels) { + reviewBody = `${BOT_COMMENT_MARKER}\nThis PR affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; + } else { + reviewBody = `${BOT_COMMENT_MARKER}\nThis PR is too large with ${nonTestChanges} line changes (excluding tests). Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\nFor guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#but-howwww-looonnnggg`; + } + + // Request changes on the PR + await github.rest.pulls.createReview({ + owner, + repo, + pull_number: pr_number, + body: reviewBody, + event: 'REQUEST_CHANGES' + }); + console.log('Created new "too big" review requesting changes'); + } else { + console.log('Skipping review creation - existing bot review already requesting changes'); + } + } else { + // Check if PR was previously too big but is now acceptable + const wasPreviouslyTooBig = currentLabels.includes('too-big'); + + if (wasPreviouslyTooBig || isMegaPR) { + console.log('PR is no longer too big or has mega-pr label - dismissing bot reviews'); + + // Get all reviews on this PR to find reviews to dismiss + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: pr_number + }); + + // Find bot reviews that requested changes + const botReviews = reviews.filter(review => + review.user.type === 'Bot' && + review.state === 'CHANGES_REQUESTED' && + review.body && review.body.includes(BOT_COMMENT_MARKER) + ); + + // Dismiss bot reviews + for (const review of botReviews) { + try { + await github.rest.pulls.dismissReview({ + owner, + repo, + pull_number: pr_number, + review_id: review.id, + message: isMegaPR ? + 'Review dismissed: mega-pr label was added' : + 'Review dismissed: PR size is now acceptable' + }); + console.log(`Dismissed review ${review.id}`); + } catch (error) { + console.log(`Failed to dismiss review ${review.id}:`, error.message); + } + } + } + } + + // 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-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml new file mode 100644 index 0000000000..1c7a62e40b --- /dev/null +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -0,0 +1,75 @@ +name: Clang-tidy Hash CI + +on: + pull_request: + paths: + - ".clang-tidy" + - "platformio.ini" + - "requirements_dev.txt" + - ".clang-tidy.hash" + - "script/clang_tidy_hash.py" + - ".github/workflows/ci-clang-tidy-hash.yml" + +permissions: + contents: read + pull-requests: write + +jobs: + verify-hash: + name: Verify clang-tidy hash + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + + - name: Set up Python + uses: actions/setup-python@v5.6.0 + with: + python-version: "3.11" + + - name: Verify hash + run: | + python script/clang_tidy_hash.py --verify + + - if: failure() + name: Show hash details + run: | + python script/clang_tidy_hash.py + echo "## Job Failed" | tee -a $GITHUB_STEP_SUMMARY + echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY + echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY + + - if: failure() + name: Request changes + uses: actions/github-script@v7.0.1 + with: + script: | + await github.rest.pulls.createReview({ + pull_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + event: 'REQUEST_CHANGES', + body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.' + }) + + - if: success() + name: Dismiss review + uses: actions/github-script@v7.0.1 + with: + script: | + let reviews = await github.rest.pulls.listReviews({ + pull_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + for (let review of reviews.data) { + if (review.user.login === 'github-actions[bot]' && review.state === 'CHANGES_REQUESTED') { + await github.rest.pulls.dismissReview({ + pull_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + review_id: review.id, + message: 'Clang-tidy hash now matches configuration.' + }); + } + } 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 b7546012e9..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 @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@v4.2.2 - name: Generate cache-key id: cache-key - run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT + run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v5.6.0 @@ -58,56 +58,16 @@ jobs: python -m venv venv . venv/bin/activate python --version - pip install -r requirements.txt -r requirements_test.txt + pip install -r requirements.txt -r requirements_test.txt pre-commit pip install -e . - ruff: - name: Check ruff - runs-on: ubuntu-24.04 - needs: - - common - steps: - - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 - - name: Restore Python - uses: ./.github/actions/restore-python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - name: Run Ruff - run: | - . venv/bin/activate - ruff format esphome tests - - name: Suggested changes - run: script/ci-suggest-changes - if: always() - - flake8: - name: Check flake8 - runs-on: ubuntu-24.04 - needs: - - common - steps: - - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 - - name: Restore Python - uses: ./.github/actions/restore-python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - name: Run flake8 - run: | - . venv/bin/activate - flake8 esphome - - name: Suggested changes - run: script/ci-suggest-changes - if: always() - pylint: name: Check pylint runs-on: ubuntu-24.04 needs: - common + - determine-jobs + if: needs.determine-jobs.outputs.python-linters == 'true' steps: - name: Check out code from GitHub uses: actions/checkout@v4.2.2 @@ -124,27 +84,6 @@ jobs: run: script/ci-suggest-changes if: always() - pyupgrade: - name: Check pyupgrade - runs-on: ubuntu-24.04 - needs: - - common - steps: - - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 - - name: Restore Python - uses: ./.github/actions/restore-python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - name: Run pyupgrade - run: | - . venv/bin/activate - pyupgrade ${{ env.PYUPGRADE_TARGET }} `find esphome -name "*.py" -type f` - - name: Suggested changes - run: script/ci-suggest-changes - if: always() - ci-custom: name: Run script/ci-custom runs-on: ubuntu-24.04 @@ -173,7 +112,6 @@ jobs: fail-fast: false matrix: python-version: - - "3.10" - "3.11" - "3.12" - "3.13" @@ -189,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 @@ -204,6 +138,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v4.2.2 - name: Restore Python + id: restore-python uses: ./.github/actions/restore-python with: python-version: ${{ matrix.python-version }} @@ -213,7 +148,7 @@ jobs: - name: Run pytest if: matrix.os == 'windows-latest' run: | - ./venv/Scripts/activate + . ./venv/Scripts/activate.ps1 pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ - name: Run pytest if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' @@ -224,12 +159,59 @@ jobs: uses: codecov/codecov-action@v5.4.3 with: token: ${{ secrets.CODECOV_TOKEN }} + - name: Save Python virtual environment cache + if: github.ref == 'refs/heads/dev' + uses: actions/cache/save@v4.2.3 + with: + path: venv + key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} + + determine-jobs: + name: Determine which jobs to run + runs-on: ubuntu-24.04 + needs: + - common + outputs: + integration-tests: ${{ steps.determine.outputs.integration-tests }} + clang-tidy: ${{ steps.determine.outputs.clang-tidy }} + python-linters: ${{ steps.determine.outputs.python-linters }} + changed-components: ${{ steps.determine.outputs.changed-components }} + component-test-count: ${{ steps.determine.outputs.component-test-count }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.2.2 + with: + # Fetch enough history to find the merge base + fetch-depth: 2 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Determine which tests to run + id: determine + env: + GH_TOKEN: ${{ github.token }} + run: | + . venv/bin/activate + output=$(python script/determine-jobs.py) + echo "Test determination output:" + echo "$output" | jq + + # Extract individual fields + echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT + echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT + echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT + echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT + echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT integration-tests: name: Run integration tests runs-on: ubuntu-latest needs: - common + - determine-jobs + if: needs.determine-jobs.outputs.integration-tests == 'true' steps: - name: Check out code from GitHub uses: actions/checkout@v4.2.2 @@ -259,44 +241,15 @@ jobs: . venv/bin/activate pytest -vv --no-cov --tb=native -n auto tests/integration/ - clang-format: - name: Check clang-format - runs-on: ubuntu-24.04 - needs: - - common - steps: - - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 - - name: Restore Python - uses: ./.github/actions/restore-python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - name: Install clang-format - run: | - . venv/bin/activate - pip install clang-format -c requirements_dev.txt - - name: Run clang-format - run: | - . venv/bin/activate - script/clang-format -i - git diff-index --quiet HEAD -- - - name: Suggested changes - run: script/ci-suggest-changes - if: always() - clang-tidy: name: ${{ matrix.name }} runs-on: ubuntu-24.04 needs: - common - - ruff - - ci-custom - - clang-format - - flake8 - - pylint - - pytest - - pyupgrade + - determine-jobs + if: needs.determine-jobs.outputs.clang-tidy == 'true' + env: + GH_TOKEN: ${{ github.token }} strategy: fail-fast: false max-parallel: 2 @@ -335,6 +288,10 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v4.2.2 + with: + # Need history for HEAD~1 to work for checking changed files + fetch-depth: 2 + - name: Restore Python uses: ./.github/actions/restore-python with: @@ -346,14 +303,14 @@ jobs: uses: actions/cache@v4.2.3 with: path: ~/.platformio - key: platformio-${{ matrix.pio_cache_key }} + key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' uses: actions/cache/restore@v4.2.3 with: path: ~/.platformio - key: platformio-${{ matrix.pio_cache_key }} + key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} - name: Register problem matchers run: | @@ -367,10 +324,28 @@ jobs: mkdir -p .temp pio run --list-targets -e esp32-idf-tidy + - name: Check if full clang-tidy scan needed + id: check_full_scan + run: | + . venv/bin/activate + if python script/clang_tidy_hash.py --check; then + echo "full_scan=true" >> $GITHUB_OUTPUT + echo "reason=hash_changed" >> $GITHUB_OUTPUT + else + echo "full_scan=false" >> $GITHUB_OUTPUT + echo "reason=normal" >> $GITHUB_OUTPUT + fi + - name: Run clang-tidy run: | . venv/bin/activate - script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }} + if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then + echo "Running FULL clang-tidy scan (hash changed)" + script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }} + else + echo "Running clang-tidy on changed files only" + script/clang-tidy --all-headers --fix --changed ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }} + fi env: # Also cache libdeps, store them in a ~/.platformio subfolder PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps @@ -380,59 +355,18 @@ jobs: # yamllint disable-line rule:line-length if: always() - list-components: - runs-on: ubuntu-24.04 - needs: - - common - if: github.event_name == 'pull_request' - outputs: - components: ${{ steps.list-components.outputs.components }} - count: ${{ steps.list-components.outputs.count }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 - with: - # Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works. - fetch-depth: 500 - - name: Get target branch - id: target-branch - run: | - echo "branch=${{ github.event.pull_request.base.ref }}" >> $GITHUB_OUTPUT - - name: Fetch ${{ steps.target-branch.outputs.branch }} branch - run: | - git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/${{ steps.target-branch.outputs.branch }}:refs/remotes/origin/${{ steps.target-branch.outputs.branch }} - git merge-base refs/remotes/origin/${{ steps.target-branch.outputs.branch }} HEAD - - name: Restore Python - uses: ./.github/actions/restore-python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - name: Find changed components - id: list-components - run: | - . venv/bin/activate - components=$(script/list-components.py --changed --branch ${{ steps.target-branch.outputs.branch }}) - output_components=$(echo "$components" | jq -R -s -c 'split("\n")[:-1] | map(select(length > 0))') - count=$(echo "$output_components" | jq length) - - echo "components=$output_components" >> $GITHUB_OUTPUT - echo "count=$count" >> $GITHUB_OUTPUT - - echo "$count Components:" - echo "$output_components" | jq - test-build-components: name: Component test ${{ matrix.file }} runs-on: ubuntu-24.04 needs: - common - - list-components - if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) > 0 && fromJSON(needs.list-components.outputs.count) < 100 + - determine-jobs + if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100 strategy: fail-fast: false max-parallel: 2 matrix: - file: ${{ fromJson(needs.list-components.outputs.components) }} + file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }} steps: - name: Install dependencies run: | @@ -460,8 +394,8 @@ jobs: runs-on: ubuntu-24.04 needs: - common - - list-components - if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100 + - determine-jobs + if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100 outputs: matrix: ${{ steps.split.outputs.components }} steps: @@ -470,7 +404,7 @@ jobs: - name: Split components into 20 groups id: split run: | - components=$(echo '${{ needs.list-components.outputs.components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]') + components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]') echo "components=$components" >> $GITHUB_OUTPUT test-build-components-split: @@ -478,9 +412,9 @@ jobs: runs-on: ubuntu-24.04 needs: - common - - list-components + - determine-jobs - test-build-components-splitter - if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100 + if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100 strategy: fail-fast: false max-parallel: 4 @@ -517,24 +451,41 @@ jobs: ./script/test_build_components -e compile -c $component done + pre-commit-ci-lite: + name: pre-commit.ci lite + runs-on: ubuntu-latest + needs: + - common + if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release' + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.2.2 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - uses: pre-commit/action@v3.0.1 + env: + SKIP: pylint,clang-tidy-hash + - uses: pre-commit-ci/lite-action@v1.1.0 + if: always() + ci-status: name: CI Status runs-on: ubuntu-24.04 needs: - common - - ruff - ci-custom - - clang-format - - flake8 - pylint - pytest - integration-tests - - pyupgrade - clang-tidy - - list-components + - determine-jobs - test-build-components - test-build-components-splitter - test-build-components-split + - pre-commit-ci-lite if: always() steps: - name: Success diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml new file mode 100644 index 0000000000..121619e049 --- /dev/null +++ b/.github/workflows/codeowner-review-request.yml @@ -0,0 +1,322 @@ +# 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`); + + // Hidden marker to identify bot comments from this workflow + const BOT_COMMENT_MARKER = ''; + + 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 `${BOT_COMMENT_MARKER}\n👋 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 `${BOT_COMMENT_MARKER}\n👋 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); + }); + + // Check for previous comments from this workflow to avoid duplicate pings + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: pr_number + }); + + const previouslyPingedUsers = new Set(); + const previouslyPingedTeams = new Set(); + + // Look for comments from github-actions bot that contain our bot marker + const workflowComments = comments.filter(comment => + comment.user.type === 'Bot' && + comment.user.login === 'github-actions[bot]' && + comment.body.includes(BOT_COMMENT_MARKER) + ); + + // Extract previously mentioned users and teams from workflow comments + for (const comment of workflowComments) { + // Match @username patterns (not team mentions) + const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || []; + userMentions.forEach(mention => { + const username = mention.slice(1); // remove @ + previouslyPingedUsers.add(username); + }); + + // Match @org/team patterns + const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/([a-zA-Z0-9_.-]+)/g) || []; + teamMentions.forEach(mention => { + const teamName = mention.split('/')[1]; + previouslyPingedTeams.add(teamName); + }); + } + + console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams`); + + // Remove users who have already been pinged in previous workflow comments + previouslyPingedUsers.forEach(user => { + matchedOwners.delete(user); + }); + + previouslyPingedTeams.forEach(team => { + matchedTeams.delete(team); + }); + + // 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, reviewed, or previously pinged)'); + 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'); + } + + // Only add a comment if there are new codeowners to mention (not previously pinged) + if (reviewersList.length > 0 || teamsList.length > 0) { + const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr_number, + body: commentBody + }); + console.log(`Added comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`); + } else { + console.log('No new codeowners to mention in comment (all previously pinged)'); + } + } catch (error) { + if (error.status === 422) { + console.log('Some reviewers may already be requested or unavailable:', error.message); + + // Only try to add a comment if there are new codeowners to mention + if (reviewersList.length > 0 || teamsList.length > 0) { + const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); + + try { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr_number, + body: commentBody + }); + console.log(`Added fallback comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`); + } catch (commentError) { + console.log('Failed to add comment:', commentError.message); + } + } else { + console.log('No new codeowners to mention in fallback comment'); + } + } 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..27976a7952 --- /dev/null +++ b/.github/workflows/issue-codeowner-notify.yml @@ -0,0 +1,158 @@ +# 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}` + ); + + // Check for previous comments from this workflow to avoid duplicate pings + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: issue_number + }); + + const previouslyPingedUsers = new Set(); + const previouslyPingedTeams = new Set(); + + // Look for comments from github-actions bot that contain codeowner pings for this component + const workflowComments = comments.filter(comment => + comment.user.type === 'Bot' && + comment.user.login === 'github-actions[bot]' && + comment.body.includes(`component: ${componentName}`) && + comment.body.includes("you've been identified as a codeowner") + ); + + // Extract previously mentioned users and teams from workflow comments + for (const comment of workflowComments) { + // Match @username patterns (not team mentions) + const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || []; + userMentions.forEach(mention => { + previouslyPingedUsers.add(mention); // Keep @ prefix for easy comparison + }); + + // Match @org/team patterns + const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+/g) || []; + teamMentions.forEach(mention => { + previouslyPingedTeams.add(mention); + }); + } + + console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams for component ${componentName}`); + + // Remove previously pinged users and teams + const newUserOwners = filteredUserOwners.filter(mention => !previouslyPingedUsers.has(mention)); + const newTeamOwners = teamOwners.filter(mention => !previouslyPingedTeams.has(mention)); + + const allMentions = [...newUserOwners, ...newTeamOwners]; + + if (allMentions.length === 0) { + console.log('No new codeowners to notify (all previously pinged or 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 new 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/.github/workflows/yaml-lint.yml b/.github/workflows/yaml-lint.yml deleted file mode 100644 index ed9b4407a2..0000000000 --- a/.github/workflows/yaml-lint.yml +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: YAML lint - -on: - push: - branches: [dev, beta, release] - paths: - - "**.yaml" - - "**.yml" - pull_request: - paths: - - "**.yaml" - - "**.yml" - -jobs: - yamllint: - name: yamllint - runs-on: ubuntu-latest - steps: - - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 - - name: Run yamllint - uses: frenck/action-yamllint@v1.5.0 - with: - strict: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 831473c325..b5b45f27aa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,17 @@ --- # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks + +ci: + autoupdate_commit_msg: 'pre-commit: autoupdate' + autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit + # Skip hooks that have issues in pre-commit CI environment + skip: [pylint, clang-tidy-hash] + repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.2 + rev: v0.12.4 hooks: # Run the linter. - id: ruff @@ -20,22 +27,25 @@ repos: - pydocstyle==5.1.1 files: ^(esphome|tests)/.+\.py$ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v5.0.0 hooks: - id: no-commit-to-branch args: - --branch=dev - --branch=release - --branch=beta + - id: end-of-file-fixer + - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade 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: - id: yamllint + exclude: ^(\.clang-format|\.clang-tidy)$ - repo: https://github.com/pre-commit/mirrors-clang-format rev: v13.0.1 hooks: @@ -48,3 +58,10 @@ repos: entry: python3 script/run-in-env.py pylint language: system types: [python] + - id: clang-tidy-hash + name: Update clang-tidy hash + entry: python script/clang_tidy_hash.py --update-if-changed + language: python + files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$ + pass_filenames: false + additional_dependencies: [] 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 ca3849eb0d..cd11c7796f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -9,6 +9,7 @@ pyproject.toml @esphome/core esphome/*.py @esphome/core esphome/core/* @esphome/core +.github/** @esphome/core # Integrations esphome/components/a01nyub/* @MrSuicideParrot @@ -28,7 +29,7 @@ esphome/components/aic3204/* @kbx81 esphome/components/airthings_ble/* @jeromelaban esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau esphome/components/airthings_wave_mini/* @ncareau -esphome/components/airthings_wave_plus/* @jeromelaban +esphome/components/airthings_wave_plus/* @jeromelaban @precurse esphome/components/alarm_control_panel/* @grahambrown11 @hwstar esphome/components/alpha3/* @jan-hofmeier esphome/components/am2315c/* @swoboda1337 @@ -170,6 +171,7 @@ esphome/components/ft5x06/* @clydebarrow esphome/components/ft63x6/* @gpambrozio esphome/components/gcja5/* @gcormier esphome/components/gdk101/* @Szewcson +esphome/components/gl_r01_i2c/* @pkejval esphome/components/globals/* @esphome/core esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp8403/* @jesserockz @@ -254,6 +256,7 @@ esphome/components/ln882x/* @lamauny esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/logger/select/* @clydebarrow +esphome/components/lps22/* @nagisa esphome/components/ltr390/* @latonita @sjtrny esphome/components/ltr501/* @latonita esphome/components/ltr_als_ps/* @latonita @@ -322,6 +325,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 @@ -376,6 +380,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 @@ -533,5 +538,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/Doxyfile b/Doxyfile index 03d432b924..1f5ac5aa1b 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.7.0-dev +PROJECT_NUMBER = 2025.8.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/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/__main__.py b/esphome/__main__.py index d8a79c018a..658aef4722 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -34,6 +34,7 @@ from esphome.const import ( CONF_PORT, CONF_SUBSTITUTIONS, CONF_TOPIC, + ENV_NOGITIGNORE, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -209,6 +210,9 @@ def wrap_to_code(name, comp): def write_cpp(config): + if not get_bool_env(ENV_NOGITIGNORE): + writer.write_gitignore() + generate_cpp_contents(config) return write_cpp_file() @@ -225,10 +229,13 @@ def generate_cpp_contents(config): def write_cpp_file(): - writer.write_platformio_project() - code_s = indent(CORE.cpp_main_section) writer.write_cpp(code_s) + + from esphome.build_gen import platformio + + platformio.write_project() + return 0 diff --git a/esphome/build_gen/__init__.py b/esphome/build_gen/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/build_gen/platformio.py b/esphome/build_gen/platformio.py new file mode 100644 index 0000000000..9bbe86694b --- /dev/null +++ b/esphome/build_gen/platformio.py @@ -0,0 +1,102 @@ +import os + +from esphome.const import __version__ +from esphome.core import CORE +from esphome.helpers import mkdir_p, read_file, write_file_if_changed +from esphome.writer import find_begin_end, update_storage_json + +INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ===========" +INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============" + +INI_BASE_FORMAT = ( + """; Auto generated code by esphome + +[common] +lib_deps = +build_flags = +upload_flags = + +""", + """ + +""", +) + + +def format_ini(data: dict[str, str | list[str]]) -> str: + content = "" + for key, value in sorted(data.items()): + if isinstance(value, list): + content += f"{key} =\n" + for x in value: + content += f" {x}\n" + else: + content += f"{key} = {value}\n" + return content + + +def get_ini_content(): + CORE.add_platformio_option( + "lib_deps", + [x.as_lib_dep for x in CORE.platformio_libraries.values()] + + ["${common.lib_deps}"], + ) + # Sort to avoid changing build flags order + CORE.add_platformio_option("build_flags", sorted(CORE.build_flags)) + + # Sort to avoid changing build unflags order + CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) + + # Add extra script for C++ flags + CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"]) + + content = "[platformio]\n" + content += f"description = ESPHome {__version__}\n" + + content += f"[env:{CORE.name}]\n" + content += format_ini(CORE.platformio_options) + + return content + + +def write_ini(content): + update_storage_json() + path = CORE.relative_build_path("platformio.ini") + + if os.path.isfile(path): + text = read_file(path) + content_format = find_begin_end( + text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END + ) + else: + content_format = INI_BASE_FORMAT + full_file = f"{content_format[0] + INI_AUTO_GENERATE_BEGIN}\n{content}" + full_file += INI_AUTO_GENERATE_END + content_format[1] + write_file_if_changed(path, full_file) + + +def write_project(): + mkdir_p(CORE.build_path) + + content = get_ini_content() + write_ini(content) + + # Write extra script for C++ specific flags + write_cxx_flags_script() + + +CXX_FLAGS_FILE_NAME = "cxx_flags.py" +CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags +Import("env") + +# Add C++ specific flags +""" + + +def write_cxx_flags_script() -> None: + path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME) + contents = CXX_FLAGS_FILE_CONTENTS + if not CORE.is_host: + contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])' + contents += "\n" + write_file_if_changed(path, contents) 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/airthings_wave_plus/__init__.py b/esphome/components/airthings_wave_plus/__init__.py index 1aff461edd..e26bfd471b 100644 --- a/esphome/components/airthings_wave_plus/__init__.py +++ b/esphome/components/airthings_wave_plus/__init__.py @@ -1 +1 @@ -CODEOWNERS = ["@jeromelaban"] +CODEOWNERS = ["@jeromelaban", "@precurse"] diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp index 8c8c514fdb..5ed62fff62 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp @@ -73,11 +73,29 @@ void AirthingsWavePlus::dump_config() { LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_); } -AirthingsWavePlus::AirthingsWavePlus() { - this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID); - this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); +void AirthingsWavePlus::setup() { + const char *service_uuid; + const char *characteristic_uuid; + const char *access_control_point_characteristic_uuid; + + // Change UUIDs for Wave Radon Gen2 + switch (this->wave_device_type_) { + case WaveDeviceType::WAVE_GEN2: + service_uuid = SERVICE_UUID_WAVE_RADON_GEN2; + characteristic_uuid = CHARACTERISTIC_UUID_WAVE_RADON_GEN2; + access_control_point_characteristic_uuid = ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID_WAVE_RADON_GEN2; + break; + default: + // Wave Plus + service_uuid = SERVICE_UUID; + characteristic_uuid = CHARACTERISTIC_UUID; + access_control_point_characteristic_uuid = ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID; + } + + this->service_uuid_ = espbt::ESPBTUUID::from_raw(service_uuid); + this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(characteristic_uuid); this->access_control_point_characteristic_uuid_ = - espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID); + espbt::ESPBTUUID::from_raw(access_control_point_characteristic_uuid); } } // namespace airthings_wave_plus diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.h b/esphome/components/airthings_wave_plus/airthings_wave_plus.h index bd7a40ef8b..c978a9af92 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.h +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.h @@ -9,13 +9,20 @@ namespace airthings_wave_plus { namespace espbt = esphome::esp32_ble_tracker; +enum WaveDeviceType : uint8_t { WAVE_PLUS = 0, WAVE_GEN2 = 1 }; + static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba"; static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-ade7-11e4-89d3-123b93f75cba"; +static const char *const SERVICE_UUID_WAVE_RADON_GEN2 = "b42e4a8e-ade7-11e4-89d3-123b93f75cba"; +static const char *const CHARACTERISTIC_UUID_WAVE_RADON_GEN2 = "b42e4dcc-ade7-11e4-89d3-123b93f75cba"; +static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID_WAVE_RADON_GEN2 = + "b42e50d8-ade7-11e4-89d3-123b93f75cba"; + class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { public: - AirthingsWavePlus(); + void setup() override; void dump_config() override; @@ -23,12 +30,14 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; } + void set_device_type(WaveDeviceType wave_device_type) { wave_device_type_ = wave_device_type; } protected: bool is_valid_radon_value_(uint16_t radon); bool is_valid_co2_value_(uint16_t co2); void read_sensors(uint8_t *raw_value, uint16_t value_len) override; + WaveDeviceType wave_device_type_{WaveDeviceType::WAVE_PLUS}; sensor::Sensor *radon_sensor_{nullptr}; sensor::Sensor *radon_long_term_sensor_{nullptr}; diff --git a/esphome/components/airthings_wave_plus/sensor.py b/esphome/components/airthings_wave_plus/sensor.py index e0e90735f0..a12c70f04c 100644 --- a/esphome/components/airthings_wave_plus/sensor.py +++ b/esphome/components/airthings_wave_plus/sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_ILLUMINANCE, CONF_RADON, CONF_RADON_LONG_TERM, + CONF_TVOC, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_ILLUMINANCE, ICON_RADIOACTIVE, @@ -15,6 +16,7 @@ from esphome.const import ( UNIT_LUX, UNIT_PARTS_PER_MILLION, ) +from esphome.types import ConfigType DEPENDENCIES = airthings_wave_base.DEPENDENCIES @@ -25,35 +27,59 @@ AirthingsWavePlus = airthings_wave_plus_ns.class_( "AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase ) +CONF_DEVICE_TYPE = "device_type" +WaveDeviceType = airthings_wave_plus_ns.enum("WaveDeviceType") +DEVICE_TYPES = { + "WAVE_PLUS": WaveDeviceType.WAVE_PLUS, + "WAVE_GEN2": WaveDeviceType.WAVE_GEN2, +} -CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(AirthingsWavePlus), - cv.Optional(CONF_RADON): sensor.sensor_schema( - unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, - icon=ICON_RADIOACTIVE, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( - unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, - icon=ICON_RADIOACTIVE, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_CO2): sensor.sensor_schema( - unit_of_measurement=UNIT_PARTS_PER_MILLION, - accuracy_decimals=0, - device_class=DEVICE_CLASS_CARBON_DIOXIDE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( - unit_of_measurement=UNIT_LUX, - accuracy_decimals=0, - device_class=DEVICE_CLASS_ILLUMINANCE, - state_class=STATE_CLASS_MEASUREMENT, - ), - } + +def validate_wave_gen2_config(config: ConfigType) -> ConfigType: + """Validate that Wave Gen2 devices don't have CO2 or TVOC sensors.""" + if config[CONF_DEVICE_TYPE] == "WAVE_GEN2": + if CONF_CO2 in config: + raise cv.Invalid("Wave Gen2 devices do not support CO2 sensor") + # Check for TVOC in the base schema config + if CONF_TVOC in config: + raise cv.Invalid("Wave Gen2 devices do not support TVOC sensor") + return config + + +CONFIG_SCHEMA = cv.All( + airthings_wave_base.BASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(AirthingsWavePlus), + cv.Optional(CONF_RADON): sensor.sensor_schema( + unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, + icon=ICON_RADIOACTIVE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( + unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, + icon=ICON_RADIOACTIVE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DEVICE_TYPE, default="WAVE_PLUS"): cv.enum( + DEVICE_TYPES, upper=True + ), + } + ), + validate_wave_gen2_config, ) @@ -73,3 +99,4 @@ async def to_code(config): if config_illuminance := config.get(CONF_ILLUMINANCE): sens = await sensor.new_sensor(config_illuminance) cg.add(var.set_illuminance(sens)) + cg.add(var.set_device_type(config[CONF_DEVICE_TYPE])) diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 4a6ce371e5..b736e6b8b0 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -23,7 +23,7 @@ void APDS9960::setup() { return; } - if (id != 0xAB && id != 0x9C && id != 0xA8) { // APDS9960 all should have one of these IDs + if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs this->error_code_ = WRONG_ID; this->mark_failed(); return; diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index eb8883b025..9cbab8164f 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -24,8 +24,9 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VARIABLES, ) -from esphome.core import coroutine_with_priority +from esphome.core import CORE, coroutine_with_priority +DOMAIN = "api" DEPENDENCIES = ["network"] AUTO_LOAD = ["socket"] CODEOWNERS = ["@OttoWinter"] @@ -51,6 +52,7 @@ SERVICE_ARG_NATIVE_TYPES = { } CONF_ENCRYPTION = "encryption" CONF_BATCH_DELAY = "batch_delay" +CONF_CUSTOM_SERVICES = "custom_services" def validate_encryption_key(value): @@ -115,6 +117,7 @@ CONFIG_SCHEMA = cv.All( cv.positive_time_period_milliseconds, cv.Range(max=cv.TimePeriod(milliseconds=65535)), ), + cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean, cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( single=True ), @@ -139,8 +142,11 @@ async def to_code(config): cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) + # Set USE_API_SERVICES if any services are enabled + if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: + cg.add_define("USE_API_SERVICES") + if actions := config.get(CONF_ACTIONS, []): - cg.add_define("USE_API_YAML_SERVICES") for conf in actions: template_args = [] func_args = [] @@ -317,7 +323,11 @@ async def api_connected_to_code(config, condition_id, template_arg, args): def FILTER_SOURCE_FILES() -> list[str]: - """Filter out api_pb2_dump.cpp when proto message dumping is not enabled.""" + """Filter out api_pb2_dump.cpp when proto message dumping is not enabled, + user_services.cpp when no services are defined, and protocol-specific + implementations based on encryption configuration.""" + files_to_filter: list[str] = [] + # api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined # This is a particularly large file that still needs to be opened and read # all the way to the end even when ifdef'd out @@ -325,6 +335,23 @@ def FILTER_SOURCE_FILES() -> list[str]: # HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set, # which happens when the logger level is VERY_VERBOSE if get_logger_level() != "VERY_VERBOSE": - return ["api_pb2_dump.cpp"] + files_to_filter.append("api_pb2_dump.cpp") - return [] + # user_services.cpp is only needed when services are defined + config = CORE.config.get(DOMAIN, {}) + if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]: + files_to_filter.append("user_services.cpp") + + # Filter protocol-specific implementations based on encryption configuration + encryption_config = config.get(CONF_ENCRYPTION) if config else None + + # If encryption is not configured at all, we only need plaintext + if encryption_config is None: + files_to_filter.append("api_frame_helper_noise.cpp") + # If encryption is configured with a key, we only need noise + elif encryption_config.get(CONF_KEY): + files_to_filter.append("api_frame_helper_plaintext.cpp") + # If encryption is configured but no key is provided, we need both + # (this allows a plaintext client to provide a noise key) + + return files_to_filter diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index c3795bb796..fd08e87bbf 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -203,7 +203,7 @@ message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; - bool uses_password = 1; + bool uses_password = 1 [(field_ifdef) = "USE_API_PASSWORD"]; // The name of the node, given by "App.set_name()" string name = 2; @@ -222,37 +222,39 @@ 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; + // Deprecated in API version 1.9 + uint32 legacy_bluetooth_proxy_version = 11 [deprecated=true, (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; + // Deprecated in API version 1.10 + uint32 legacy_voice_assistant_version = 14 [deprecated=true, (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 +292,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 +313,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,20 +326,22 @@ 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"]; } +// Deprecated in API version 1.1 enum LegacyCoverState { + option deprecated = true; LEGACY_COVER_STATE_OPEN = 0; LEGACY_COVER_STATE_CLOSED = 1; } @@ -356,15 +360,18 @@ message CoverStateResponse { fixed32 key = 1; // legacy: state has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change - LegacyCoverState legacy_state = 2; + // Deprecated in API version 1.1 + LegacyCoverState legacy_state = 2 [deprecated=true]; float position = 3; float tilt = 4; CoverOperation current_operation = 5; - uint32 device_id = 6; + uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"]; } +// Deprecated in API version 1.1 enum LegacyCoverCommand { + option deprecated = true; LEGACY_COVER_COMMAND_OPEN = 0; LEGACY_COVER_COMMAND_CLOSE = 1; LEGACY_COVER_COMMAND_STOP = 2; @@ -374,19 +381,23 @@ message CoverCommandRequest { option (source) = SOURCE_CLIENT; option (ifdef) = "USE_COVER"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; // legacy: command has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change - bool has_legacy_command = 2; - LegacyCoverCommand legacy_command = 3; + // Deprecated in API version 1.1 + bool has_legacy_command = 2 [deprecated=true]; + // Deprecated in API version 1.1 + LegacyCoverCommand legacy_command = 3 [deprecated=true]; bool has_position = 4; float position = 5; bool has_tilt = 6; float tilt = 7; bool stop = 8; + uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } // ==================== FAN ==================== @@ -399,19 +410,21 @@ 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"]; } +// Deprecated in API version 1.6 - only used in deprecated fields enum FanSpeed { + option deprecated = true; FAN_SPEED_LOW = 0; FAN_SPEED_MEDIUM = 1; FAN_SPEED_HIGH = 2; @@ -430,23 +443,27 @@ message FanStateResponse { fixed32 key = 1; bool state = 2; bool oscillating = 3; - FanSpeed speed = 4 [deprecated = true]; + // Deprecated in API version 1.6 + FanSpeed speed = 4 [deprecated=true]; 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_FAN"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; bool has_state = 2; bool state = 3; - bool has_speed = 4 [deprecated = true]; - FanSpeed speed = 5 [deprecated = true]; + // Deprecated in API version 1.6 + bool has_speed = 4 [deprecated=true]; + // Deprecated in API version 1.6 + FanSpeed speed = 5 [deprecated=true]; bool has_oscillating = 6; bool oscillating = 7; bool has_direction = 8; @@ -455,6 +472,7 @@ message FanCommandRequest { int32 speed_level = 11; bool has_preset_mode = 12; string preset_mode = 13; + uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; } // ==================== LIGHT ==================== @@ -480,21 +498,25 @@ 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 + // Deprecated in API version 1.6 bool legacy_supports_brightness = 5 [deprecated=true]; + // Deprecated in API version 1.6 bool legacy_supports_rgb = 6 [deprecated=true]; + // Deprecated in API version 1.6 bool legacy_supports_white_value = 7 [deprecated=true]; + // Deprecated in API version 1.6 bool legacy_supports_color_temperature = 8 [deprecated=true]; float min_mireds = 9; 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; @@ -516,13 +538,14 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_LIGHT"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; bool has_state = 2; @@ -551,6 +574,7 @@ message LightCommandRequest { uint32 flash_length = 17; bool has_effect = 18; string effect = 19; + uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"]; } // ==================== SENSOR ==================== @@ -561,7 +585,9 @@ enum SensorStateClass { STATE_CLASS_TOTAL = 3; } +// Deprecated in API version 1.5 enum SensorLastResetType { + option deprecated = true; LAST_RESET_NONE = 0; LAST_RESET_NEVER = 1; LAST_RESET_AUTO = 2; @@ -576,19 +602,20 @@ 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; string device_class = 9; SensorStateClass state_class = 10; // Last reset type removed in 2021.9.0 - SensorLastResetType legacy_last_reset_type = 11; + // Deprecated in API version 1.5 + SensorLastResetType legacy_last_reset_type = 11 [deprecated=true]; 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; @@ -602,7 +629,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 ==================== @@ -615,14 +642,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; @@ -633,16 +660,18 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_SWITCH"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; bool state = 2; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== TEXT SENSOR ==================== @@ -655,13 +684,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; @@ -675,7 +704,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 ==================== @@ -799,18 +828,21 @@ enum ServiceArgType { SERVICE_ARG_TYPE_STRING_ARRAY = 7; } message ListEntitiesServicesArgument { + option (ifdef) = "USE_API_SERVICES"; string name = 1; ServiceArgType type = 2; } message ListEntitiesServicesResponse { option (id) = 41; option (source) = SOURCE_SERVER; + option (ifdef) = "USE_API_SERVICES"; string name = 1; fixed32 key = 2; repeated ListEntitiesServicesArgument args = 3; } message ExecuteServiceArgument { + option (ifdef) = "USE_API_SERVICES"; bool bool_ = 1; int32 legacy_int = 2; float float_ = 3; @@ -826,6 +858,7 @@ message ExecuteServiceRequest { option (id) = 42; option (source) = SOURCE_CLIENT; option (no_delay) = true; + option (ifdef) = "USE_API_SERVICES"; fixed32 key = 1; repeated ExecuteServiceArgument args = 2; @@ -841,21 +874,23 @@ 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 { option (id) = 44; + option (base_class) = "StateResponseProtoMessage"; option (source) = SOURCE_SERVER; option (ifdef) = "USE_CAMERA"; fixed32 key = 1; bytes data = 2; bool done = 3; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message CameraImageRequest { option (id) = 45; @@ -923,7 +958,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; @@ -933,7 +968,8 @@ message ListEntitiesClimateResponse { float visual_target_temperature_step = 10; // for older peer versions - in new system this // is if CLIMATE_PRESET_AWAY exists is supported_presets - bool legacy_supports_away = 11; + // Deprecated in API version 1.5 + bool legacy_supports_away = 11 [deprecated=true]; bool supports_action = 12; repeated ClimateFanMode supported_fan_modes = 13; repeated ClimateSwingMode supported_swing_modes = 14; @@ -941,14 +977,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; @@ -964,7 +1000,8 @@ message ClimateStateResponse { float target_temperature_low = 5; float target_temperature_high = 6; // For older peers, equal to preset == CLIMATE_PRESET_AWAY - bool unused_legacy_away = 7; + // Deprecated in API version 1.5 + bool unused_legacy_away = 7 [deprecated=true]; ClimateAction action = 8; ClimateFanMode fan_mode = 9; ClimateSwingMode swing_mode = 10; @@ -973,13 +1010,14 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_CLIMATE"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; bool has_mode = 2; @@ -991,8 +1029,10 @@ message ClimateCommandRequest { bool has_target_temperature_high = 8; float target_temperature_high = 9; // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset - bool unused_has_legacy_away = 10; - bool unused_legacy_away = 11; + // Deprecated in API version 1.5 + bool unused_has_legacy_away = 10 [deprecated=true]; + // Deprecated in API version 1.5 + bool unused_legacy_away = 11 [deprecated=true]; bool has_fan_mode = 12; ClimateFanMode fan_mode = 13; bool has_swing_mode = 14; @@ -1005,6 +1045,7 @@ message ClimateCommandRequest { string custom_preset = 21; bool has_target_humidity = 22; float target_humidity = 23; + uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"]; } // ==================== NUMBER ==================== @@ -1022,9 +1063,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; @@ -1033,7 +1074,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; @@ -1047,16 +1088,18 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_NUMBER"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; float state = 2; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== SELECT ==================== @@ -1069,13 +1112,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; @@ -1089,16 +1132,18 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_SELECT"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; string state = 2; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== SIREN ==================== @@ -1111,15 +1156,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; @@ -1130,13 +1175,14 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_SIREN"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; bool has_state = 2; @@ -1147,6 +1193,7 @@ message SirenCommandRequest { uint32 duration = 7; bool has_volume = 8; float volume = 9; + uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } // ==================== LOCK ==================== @@ -1172,9 +1219,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; @@ -1184,7 +1231,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; @@ -1194,19 +1241,21 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_LOCK"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; LockCommand command = 2; // Not yet implemented: bool has_code = 3; string code = 4; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } // ==================== BUTTON ==================== @@ -1219,21 +1268,23 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_BUTTON"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; + uint32 device_id = 2 [(field_ifdef) = "USE_DEVICES"]; } // ==================== MEDIA PLAYER ==================== @@ -1272,9 +1323,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; @@ -1282,7 +1333,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; @@ -1294,13 +1345,14 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_MEDIA_PLAYER"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; @@ -1315,6 +1367,7 @@ message MediaPlayerCommandRequest { bool has_announcement = 8; bool announcement = 9; + uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } // ==================== BLUETOOTH ==================== @@ -1326,12 +1379,17 @@ message SubscribeBluetoothLEAdvertisementsRequest { uint32 flags = 1; } +// Deprecated - only used by deprecated BluetoothLEAdvertisementResponse message BluetoothServiceData { + option deprecated = true; string uuid = 1; - repeated uint32 legacy_data = 2 [deprecated = true]; // Removed in api version 1.7 + // Deprecated in API version 1.7 + repeated uint32 legacy_data = 2 [deprecated=true]; // Removed in api version 1.7 bytes data = 3; // Added in api version 1.7 } +// Removed in ESPHome 2025.8.0 - use BluetoothLERawAdvertisementsResponse instead message BluetoothLEAdvertisementResponse { + option deprecated = true; option (id) = 67; option (source) = SOURCE_SERVER; option (ifdef) = "USE_BLUETOOTH_PROXY"; @@ -1353,7 +1411,7 @@ message BluetoothLERawAdvertisement { sint32 rssi = 2; uint32 address_type = 3; - bytes data = 4; + bytes data = 4 [(fixed_array_size) = 62]; } message BluetoothLERawAdvertisementsResponse { @@ -1817,14 +1875,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 { @@ -1835,7 +1893,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 { @@ -1843,9 +1901,11 @@ message AlarmControlPanelCommandRequest { option (source) = SOURCE_CLIENT; option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; AlarmControlPanelStateCommand command = 2; string code = 3; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } // ===================== TEXT ===================== @@ -1862,8 +1922,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; @@ -1871,7 +1931,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; @@ -1885,16 +1945,18 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_TEXT"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; string state = 2; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1908,12 +1970,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; @@ -1929,18 +1991,20 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_DATETIME_DATE"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; uint32 year = 2; uint32 month = 3; uint32 day = 4; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } // ==================== DATETIME TIME ==================== @@ -1953,12 +2017,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; @@ -1974,18 +2038,20 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_DATETIME_TIME"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; uint32 hour = 2; uint32 minute = 3; uint32 second = 4; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } // ==================== EVENT ==================== @@ -1998,15 +2064,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; @@ -2016,7 +2082,7 @@ message EventResponse { fixed32 key = 1; string event_type = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== VALVE ==================== @@ -2029,9 +2095,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; @@ -2039,7 +2105,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 { @@ -2057,7 +2123,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 { @@ -2065,11 +2131,13 @@ message ValveCommandRequest { option (source) = SOURCE_CLIENT; option (ifdef) = "USE_VALVE"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; bool has_position = 2; float position = 3; bool stop = 4; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } // ==================== DATETIME DATETIME ==================== @@ -2082,12 +2150,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; @@ -2101,16 +2169,18 @@ 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; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_DATETIME_DATETIME"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; fixed32 epoch_seconds = 2; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== UPDATE ==================== @@ -2123,13 +2193,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; @@ -2148,7 +2218,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; @@ -2160,7 +2230,9 @@ message UpdateCommandRequest { option (source) = SOURCE_CLIENT; option (ifdef) = "USE_UPDATE"; option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; fixed32 key = 1; UpdateCommand command = 2; + 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 779784e787..bc0afd49eb 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1,5 +1,11 @@ #include "api_connection.h" #ifdef USE_API +#ifdef USE_API_NOISE +#include "api_frame_helper_noise.h" +#endif +#ifdef USE_API_PLAINTEXT +#include "api_frame_helper_plaintext.h" +#endif #include #include #include @@ -42,32 +48,53 @@ 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) { #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) auto noise_ctx = parent->get_noise_ctx(); if (noise_ctx->has_psk()) { - this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx)}; + this->helper_ = + std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)}; } else { - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; } #elif defined(USE_API_PLAINTEXT) - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; #elif defined(USE_API_NOISE) - this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; + this->helper_ = std::unique_ptr{ + new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx(), &this->client_info_)}; #else #error "No frame helper defined" #endif @@ -86,13 +113,12 @@ 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(); - this->client_peername_ = this->client_info_; - this->helper_->set_log_info(this->client_info_); + this->client_info_.peername = helper_->getpeername(); + this->client_info_.name = this->client_info_.peername; } APIConnection::~APIConnection() { @@ -119,7 +145,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 +162,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,21 +206,24 @@ 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 ESP_LOGW(TAG, "Buffer full, ping queued"); - this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE); + this->schedule_message_front_(nullptr, &APIConnection::try_send_ping_request, PingRequest::MESSAGE_TYPE, + PingRequest::ESTIMATED_SIZE); this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings } } #ifdef USE_CAMERA if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) { - uint32_t to_send = std::min((size_t) MAX_PACKET_SIZE, this->image_reader_->available()); + uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available()); bool done = this->image_reader_->available() == to_send; uint32_t msg_size = 0; ProtoSize::add_fixed_field<4>(msg_size, 1, true); @@ -236,7 +259,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 { @@ -245,10 +268,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 @@ -265,7 +284,7 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) { // Encodes a message to the buffer and returns the total number of bytes used, // including header and footer overhead. Returns 0 if the message doesn't fit. -uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, +uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { #ifdef HAS_PROTO_MESSAGE_DUMP // If in log-only mode, just log and return @@ -316,7 +335,7 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes #ifdef USE_BINARY_SENSOR bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) { return this->send_message_smart_(binary_sensor, &APIConnection::try_send_binary_sensor_state, - BinarySensorStateResponse::MESSAGE_TYPE); + BinarySensorStateResponse::MESSAGE_TYPE, BinarySensorStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -325,8 +344,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, @@ -335,29 +354,26 @@ 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 #ifdef USE_COVER bool APIConnection::send_cover_state(cover::Cover *cover) { - return this->send_message_smart_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(cover, &APIConnection::try_send_cover_state, CoverStateResponse::MESSAGE_TYPE, + CoverStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *cover = static_cast(entity); CoverStateResponse msg; auto traits = cover->get_traits(); - msg.legacy_state = - (cover->position == cover::COVER_OPEN) ? enums::LEGACY_COVER_STATE_OPEN : enums::LEGACY_COVER_STATE_CLOSED; msg.position = cover->position; 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) { @@ -369,25 +385,11 @@ 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) - if (msg.has_legacy_command) { - switch (msg.legacy_command) { - case enums::LEGACY_COVER_COMMAND_OPEN: - call.set_command_open(); - break; - case enums::LEGACY_COVER_COMMAND_CLOSE: - call.set_command_close(); - break; - case enums::LEGACY_COVER_COMMAND_STOP: - call.set_command_stop(); - break; - } - } if (msg.has_position) call.set_position(msg.position); if (msg.has_tilt) @@ -400,7 +402,8 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { #ifdef USE_FAN bool APIConnection::send_fan_state(fan::Fan *fan) { - return this->send_message_smart_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(fan, &APIConnection::try_send_fan_state, FanStateResponse::MESSAGE_TYPE, + FanStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -417,8 +420,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) { @@ -431,9 +433,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) @@ -455,7 +455,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { #ifdef USE_LIGHT bool APIConnection::send_light_state(light::LightState *light) { - return this->send_message_smart_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(light, &APIConnection::try_send_light_state, LightStateResponse::MESSAGE_TYPE, + LightStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -477,8 +478,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) { @@ -487,14 +487,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c auto traits = light->get_traits(); for (auto mode : traits.get_supported_color_modes()) msg.supported_color_modes.push_back(static_cast(mode)); - msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS); - msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB); - msg.legacy_supports_white_value = - msg.legacy_supports_rgb && (traits.supports_color_capability(light::ColorCapability::WHITE) || - traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)); - msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || - traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE); - if (msg.legacy_supports_color_temperature) { + if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || + traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { msg.min_mireds = traits.get_min_mireds(); msg.max_mireds = traits.get_max_mireds(); } @@ -504,9 +498,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) @@ -543,7 +536,8 @@ void APIConnection::light_command(const LightCommandRequest &msg) { #ifdef USE_SENSOR bool APIConnection::send_sensor_state(sensor::Sensor *sensor) { - return this->send_message_smart_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(sensor, &APIConnection::try_send_sensor_state, SensorStateResponse::MESSAGE_TYPE, + SensorStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -552,8 +546,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, @@ -565,17 +558,15 @@ 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 #ifdef USE_SWITCH bool APIConnection::send_switch_state(switch_::Switch *a_switch) { - return this->send_message_smart_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(a_switch, &APIConnection::try_send_switch_state, SwitchStateResponse::MESSAGE_TYPE, + SwitchStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -583,8 +574,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, @@ -593,9 +584,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) @@ -611,7 +601,7 @@ void APIConnection::switch_command(const SwitchCommandRequest &msg) { #ifdef USE_TEXT_SENSOR bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) { return this->send_message_smart_(text_sensor, &APIConnection::try_send_text_sensor_state, - TextSensorStateResponse::MESSAGE_TYPE); + TextSensorStateResponse::MESSAGE_TYPE, TextSensorStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -620,31 +610,28 @@ 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 #ifdef USE_CLIMATE bool APIConnection::send_climate_state(climate::Climate *climate) { - return this->send_message_smart_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(climate, &APIConnection::try_send_climate_state, ClimateStateResponse::MESSAGE_TYPE, + ClimateStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, 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); @@ -671,7 +658,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) { @@ -690,7 +678,6 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity(); - msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.supports_action = traits.get_supports_action(); for (auto fan_mode : traits.get_supported_fan_modes()) msg.supported_fan_modes.push_back(static_cast(fan_mode)); @@ -702,9 +689,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) @@ -734,7 +720,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { #ifdef USE_NUMBER bool APIConnection::send_number_state(number::Number *number) { - return this->send_message_smart_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(number, &APIConnection::try_send_number_state, NumberStateResponse::MESSAGE_TYPE, + NumberStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -743,8 +730,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, @@ -757,9 +743,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) @@ -770,7 +755,8 @@ void APIConnection::number_command(const NumberCommandRequest &msg) { #ifdef USE_DATETIME_DATE bool APIConnection::send_date_state(datetime::DateEntity *date) { - return this->send_message_smart_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(date, &APIConnection::try_send_date_state, DateStateResponse::MESSAGE_TYPE, + DateStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -780,16 +766,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) @@ -800,7 +784,8 @@ void APIConnection::date_command(const DateCommandRequest &msg) { #ifdef USE_DATETIME_TIME bool APIConnection::send_time_state(datetime::TimeEntity *time) { - return this->send_message_smart_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(time, &APIConnection::try_send_time_state, TimeStateResponse::MESSAGE_TYPE, + TimeStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -810,16 +795,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) @@ -831,7 +814,7 @@ void APIConnection::time_command(const TimeCommandRequest &msg) { #ifdef USE_DATETIME_DATETIME bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { return this->send_message_smart_(datetime, &APIConnection::try_send_datetime_state, - DateTimeStateResponse::MESSAGE_TYPE); + DateTimeStateResponse::MESSAGE_TYPE, DateTimeStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -842,16 +825,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) @@ -862,7 +844,8 @@ void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { #ifdef USE_TEXT bool APIConnection::send_text_state(text::Text *text) { - return this->send_message_smart_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(text, &APIConnection::try_send_text_state, TextStateResponse::MESSAGE_TYPE, + TextStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -871,8 +854,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, @@ -883,9 +865,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) @@ -896,7 +877,8 @@ void APIConnection::text_command(const TextCommandRequest &msg) { #ifdef USE_SELECT bool APIConnection::send_select_state(select::Select *select) { - return this->send_message_smart_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(select, &APIConnection::try_send_select_state, SelectStateResponse::MESSAGE_TYPE, + SelectStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -905,8 +887,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, @@ -915,9 +896,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) @@ -932,9 +912,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) @@ -944,7 +923,8 @@ void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg #ifdef USE_LOCK bool APIConnection::send_lock_state(lock::Lock *a_lock) { - return this->send_message_smart_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(a_lock, &APIConnection::try_send_lock_state, LockStateResponse::MESSAGE_TYPE, + LockStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -952,8 +932,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, @@ -963,9 +942,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) @@ -986,7 +964,8 @@ void APIConnection::lock_command(const LockCommandRequest &msg) { #ifdef USE_VALVE bool APIConnection::send_valve_state(valve::Valve *valve) { - return this->send_message_smart_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(valve, &APIConnection::try_send_valve_state, ValveStateResponse::MESSAGE_TYPE, + ValveStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -994,8 +973,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) { @@ -1006,9 +984,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) @@ -1023,7 +1000,7 @@ void APIConnection::valve_command(const ValveCommandRequest &msg) { #ifdef USE_MEDIA_PLAYER bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) { return this->send_message_smart_(media_player, &APIConnection::try_send_media_player_state, - MediaPlayerStateResponse::MESSAGE_TYPE); + MediaPlayerStateResponse::MESSAGE_TYPE, MediaPlayerStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1035,8 +1012,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) { @@ -1053,9 +1030,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) @@ -1090,9 +1066,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) @@ -1123,21 +1098,6 @@ void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoo void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); } -bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) { - if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) { - BluetoothLEAdvertisementResponse resp = msg; - for (auto &service : resp.service_data) { - service.legacy_data.assign(service.data.begin(), service.data.end()); - service.data.clear(); - } - for (auto &manufacturer_data : resp.manufacturer_data) { - 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(msg); -} void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); } @@ -1262,15 +1222,16 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon #ifdef USE_ALARM_CONTROL_PANEL bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { return this->send_message_smart_(a_alarm_control_panel, &APIConnection::try_send_alarm_control_panel_state, - AlarmControlPanelStateResponse::MESSAGE_TYPE); + AlarmControlPanelStateResponse::MESSAGE_TYPE, + AlarmControlPanelStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { 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) { @@ -1279,10 +1240,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) @@ -1316,14 +1275,14 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #ifdef USE_EVENT void APIConnection::send_event(event::Event *event, const std::string &event_type) { - this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE); + this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, + EventResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, 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, @@ -1333,15 +1292,15 @@ 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 #ifdef USE_UPDATE bool APIConnection::send_update_state(update::UpdateEntity *update) { - return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE); + return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE, + UpdateStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1360,17 +1319,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) @@ -1393,9 +1350,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; @@ -1418,14 +1372,31 @@ 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_.name, this->client_info_.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(); - this->helper_->set_log_info(this->get_client_combined_info()); + this->client_info_.name = msg.client_info; + this->client_info_.peername = this->helper_->getpeername(); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; - ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(), - this->client_peername_.c_str(), this->client_api_version_major_, this->client_api_version_minor_); + ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(), + this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; resp.api_version_major = 1; @@ -1433,7 +1404,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) { @@ -1446,29 +1424,20 @@ 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(); -#else - resp.uses_password = false; + resp.uses_password = true; #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(); @@ -1497,12 +1466,10 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.webserver_port = USE_WEBSERVER_PORT; #endif #ifdef USE_BLUETOOTH_PROXY - resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version(); resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); resp.bluetooth_mac_address = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); #endif #ifdef USE_VOICE_ASSISTANT - resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version(); resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags(); #endif #ifdef USE_API_NOISE @@ -1534,6 +1501,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes } } } +#ifdef USE_API_SERVICES void APIConnection::execute_service(const ExecuteServiceRequest &msg) { bool found = false; for (auto *service : this->parent_->get_user_services()) { @@ -1545,6 +1513,7 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { ESP_LOGV(TAG, "Could not find service"); } } +#endif #ifdef USE_API_NOISE NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) { psk_t psk{}; @@ -1577,7 +1546,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; } @@ -1588,7 +1557,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { } return false; } -bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) { +bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) { // SubscribeLogsResponse return false; } @@ -1598,12 +1567,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_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 @@ -1611,18 +1576,19 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_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(); this->flags_.remove = true; } -void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type) { +void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, + uint8_t estimated_size) { // Check if we already have a message of this type for this entity // This provides deduplication per entity/message_type combination // O(n) but optimized for RAM and not performance. @@ -1637,12 +1603,20 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c } // No existing item found, add new one - items.emplace_back(entity, std::move(creator), message_type); + items.emplace_back(entity, std::move(creator), message_type, estimated_size); } -void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type) { - // Insert at front for high priority messages (no deduplication check) - items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type)); +void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, + uint8_t 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_() { @@ -1662,6 +1636,10 @@ ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) { } void APIConnection::process_batch_() { + // Ensure PacketInfo remains trivially destructible for our placement new approach + static_assert(std::is_trivially_destructible::value, + "PacketInfo must remain trivially destructible with this placement-new approach"); + if (this->deferred_batch_.empty()) { this->flags_.batch_scheduled = false; return; @@ -1699,9 +1677,12 @@ void APIConnection::process_batch_() { return; } - // Pre-allocate storage for packet info - std::vector packet_info; - packet_info.reserve(num_items); + size_t packets_to_process = std::min(num_items, MAX_PACKETS_PER_BATCH); + + // Stack-allocated array for packet info + alignas(PacketInfo) char packet_info_storage[MAX_PACKETS_PER_BATCH * sizeof(PacketInfo)]; + PacketInfo *packet_info = reinterpret_cast(packet_info_storage); + size_t packet_count = 0; // Cache these values to avoid repeated virtual calls const uint8_t header_padding = this->helper_->frame_header_padding(); @@ -1714,7 +1695,7 @@ void APIConnection::process_batch_() { uint32_t total_estimated_size = 0; for (size_t i = 0; i < this->deferred_batch_.size(); i++) { const auto &item = this->deferred_batch_[i]; - total_estimated_size += get_estimated_message_size(item.message_type); + total_estimated_size += item.estimated_size; } // Calculate total overhead for all messages @@ -1733,8 +1714,8 @@ void APIConnection::process_batch_() { // The actual message data follows after the header padding uint32_t current_offset = 0; - // Process items and encode directly to buffer - for (size_t i = 0; i < this->deferred_batch_.size(); i++) { + // Process items and encode directly to buffer (up to our limit) + for (size_t i = 0; i < packets_to_process; i++) { const auto &item = this->deferred_batch_[i]; // Try to encode message // The creator will calculate overhead to determine if the message fits @@ -1748,13 +1729,17 @@ void APIConnection::process_batch_() { // Message was encoded successfully // payload_size is header_padding + actual payload size + footer_size uint16_t proto_payload_size = payload_size - header_padding - footer_size; - packet_info.emplace_back(item.message_type, current_offset, proto_payload_size); + // Use placement new to construct PacketInfo in pre-allocated stack array + // This avoids default-constructing all MAX_PACKETS_PER_BATCH elements + // Explicit destruction is not needed because PacketInfo is trivially destructible, + // as ensured by the static_assert in its definition. + new (&packet_info[packet_count++]) PacketInfo(item.message_type, current_offset, proto_payload_size); // Update tracking variables items_processed++; - // After first message, set remaining size to MAX_PACKET_SIZE to avoid fragmentation + // After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation if (items_processed == 1) { - remaining_size = MAX_PACKET_SIZE; + remaining_size = MAX_BATCH_PACKET_SIZE; } remaining_size -= payload_size; // Calculate where the next message's header padding will start @@ -1774,16 +1759,12 @@ void APIConnection::process_batch_() { } // Send all collected packets - APIError err = - this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info); + APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, + std::span(packet_info, packet_count)); 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 @@ -1808,7 +1789,7 @@ void APIConnection::process_batch_() { } uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single, uint16_t message_type) const { + bool is_single, uint8_t message_type) const { #ifdef USE_EVENT // Special case: EventResponse uses string pointer if (message_type == EventResponse::MESSAGE_TYPE) { @@ -1839,149 +1820,6 @@ uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); } -uint16_t APIConnection::get_estimated_message_size(uint16_t message_type) { - // Use generated ESTIMATED_SIZE constants from each message type - switch (message_type) { -#ifdef USE_BINARY_SENSOR - case BinarySensorStateResponse::MESSAGE_TYPE: - return BinarySensorStateResponse::ESTIMATED_SIZE; - case ListEntitiesBinarySensorResponse::MESSAGE_TYPE: - return ListEntitiesBinarySensorResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_SENSOR - case SensorStateResponse::MESSAGE_TYPE: - return SensorStateResponse::ESTIMATED_SIZE; - case ListEntitiesSensorResponse::MESSAGE_TYPE: - return ListEntitiesSensorResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_SWITCH - case SwitchStateResponse::MESSAGE_TYPE: - return SwitchStateResponse::ESTIMATED_SIZE; - case ListEntitiesSwitchResponse::MESSAGE_TYPE: - return ListEntitiesSwitchResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_TEXT_SENSOR - case TextSensorStateResponse::MESSAGE_TYPE: - return TextSensorStateResponse::ESTIMATED_SIZE; - case ListEntitiesTextSensorResponse::MESSAGE_TYPE: - return ListEntitiesTextSensorResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_NUMBER - case NumberStateResponse::MESSAGE_TYPE: - return NumberStateResponse::ESTIMATED_SIZE; - case ListEntitiesNumberResponse::MESSAGE_TYPE: - return ListEntitiesNumberResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_TEXT - case TextStateResponse::MESSAGE_TYPE: - return TextStateResponse::ESTIMATED_SIZE; - case ListEntitiesTextResponse::MESSAGE_TYPE: - return ListEntitiesTextResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_SELECT - case SelectStateResponse::MESSAGE_TYPE: - return SelectStateResponse::ESTIMATED_SIZE; - case ListEntitiesSelectResponse::MESSAGE_TYPE: - return ListEntitiesSelectResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_LOCK - case LockStateResponse::MESSAGE_TYPE: - return LockStateResponse::ESTIMATED_SIZE; - case ListEntitiesLockResponse::MESSAGE_TYPE: - return ListEntitiesLockResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_EVENT - case EventResponse::MESSAGE_TYPE: - return EventResponse::ESTIMATED_SIZE; - case ListEntitiesEventResponse::MESSAGE_TYPE: - return ListEntitiesEventResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_COVER - case CoverStateResponse::MESSAGE_TYPE: - return CoverStateResponse::ESTIMATED_SIZE; - case ListEntitiesCoverResponse::MESSAGE_TYPE: - return ListEntitiesCoverResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_FAN - case FanStateResponse::MESSAGE_TYPE: - return FanStateResponse::ESTIMATED_SIZE; - case ListEntitiesFanResponse::MESSAGE_TYPE: - return ListEntitiesFanResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_LIGHT - case LightStateResponse::MESSAGE_TYPE: - return LightStateResponse::ESTIMATED_SIZE; - case ListEntitiesLightResponse::MESSAGE_TYPE: - return ListEntitiesLightResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_CLIMATE - case ClimateStateResponse::MESSAGE_TYPE: - return ClimateStateResponse::ESTIMATED_SIZE; - case ListEntitiesClimateResponse::MESSAGE_TYPE: - return ListEntitiesClimateResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_ESP32_CAMERA - case ListEntitiesCameraResponse::MESSAGE_TYPE: - return ListEntitiesCameraResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_BUTTON - case ListEntitiesButtonResponse::MESSAGE_TYPE: - return ListEntitiesButtonResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_MEDIA_PLAYER - case MediaPlayerStateResponse::MESSAGE_TYPE: - return MediaPlayerStateResponse::ESTIMATED_SIZE; - case ListEntitiesMediaPlayerResponse::MESSAGE_TYPE: - return ListEntitiesMediaPlayerResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_ALARM_CONTROL_PANEL - case AlarmControlPanelStateResponse::MESSAGE_TYPE: - return AlarmControlPanelStateResponse::ESTIMATED_SIZE; - case ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE: - return ListEntitiesAlarmControlPanelResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_DATETIME_DATE - case DateStateResponse::MESSAGE_TYPE: - return DateStateResponse::ESTIMATED_SIZE; - case ListEntitiesDateResponse::MESSAGE_TYPE: - return ListEntitiesDateResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_DATETIME_TIME - case TimeStateResponse::MESSAGE_TYPE: - return TimeStateResponse::ESTIMATED_SIZE; - case ListEntitiesTimeResponse::MESSAGE_TYPE: - return ListEntitiesTimeResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_DATETIME_DATETIME - case DateTimeStateResponse::MESSAGE_TYPE: - return DateTimeStateResponse::ESTIMATED_SIZE; - case ListEntitiesDateTimeResponse::MESSAGE_TYPE: - return ListEntitiesDateTimeResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_VALVE - case ValveStateResponse::MESSAGE_TYPE: - return ValveStateResponse::ESTIMATED_SIZE; - case ListEntitiesValveResponse::MESSAGE_TYPE: - return ListEntitiesValveResponse::ESTIMATED_SIZE; -#endif -#ifdef USE_UPDATE - case UpdateStateResponse::MESSAGE_TYPE: - return UpdateStateResponse::ESTIMATED_SIZE; - case ListEntitiesUpdateResponse::MESSAGE_TYPE: - return ListEntitiesUpdateResponse::ESTIMATED_SIZE; -#endif - case ListEntitiesServicesResponse::MESSAGE_TYPE: - return ListEntitiesServicesResponse::ESTIMATED_SIZE; - case ListEntitiesDoneResponse::MESSAGE_TYPE: - return ListEntitiesDoneResponse::ESTIMATED_SIZE; - case DisconnectRequest::MESSAGE_TYPE: - return DisconnectRequest::ESTIMATED_SIZE; - default: - // Fallback for unknown message types - return 24; - } -} - } // namespace api } // namespace esphome #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index b70b037999..de7e91de01 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -16,10 +16,34 @@ namespace esphome { namespace api { +// Client information structure +struct ClientInfo { + std::string name; // Client name from Hello message + std::string peername; // IP:port from socket + + std::string get_combined_info() const { + if (name == peername) { + // Before Hello message, both are the same + return name; + } + return name + " (" + peername + ")"; + } +}; + // Keepalive timeout in milliseconds static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; // Maximum number of entities to process in a single batch during initial state/info sending -static constexpr size_t MAX_INITIAL_PER_BATCH = 20; +// This was increased from 20 to 24 after removing the unique_id field from entity info messages, +// which reduced message sizes allowing more entities per batch without exceeding packet limits +static constexpr size_t MAX_INITIAL_PER_BATCH = 24; +// Maximum number of packets to process in a single batch (platform-dependent) +// This limit exists to prevent stack overflow from the PacketInfo array in process_batch_ +// Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes +#if defined(USE_ESP32) || defined(USE_HOST) +static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HOST has plenty +#else +static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks +#endif class APIConnection : public APIServerConnection { public: @@ -33,7 +57,7 @@ class APIConnection : public APIServerConnection { bool send_list_info_done() { return this->schedule_message_(nullptr, &APIConnection::try_send_list_info_done, - ListEntitiesDoneResponse::MESSAGE_TYPE); + ListEntitiesDoneResponse::MESSAGE_TYPE, ListEntitiesDoneResponse::ESTIMATED_SIZE); } #ifdef USE_BINARY_SENSOR bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor); @@ -111,12 +135,11 @@ 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; void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; - bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg); void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; @@ -133,7 +156,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 @@ -195,7 +218,9 @@ class APIConnection : public APIServerConnection { // TODO return {}; } +#ifdef USE_API_SERVICES void execute_service(const ExecuteServiceRequest &msg) override; +#endif #ifdef USE_API_NOISE NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; #endif @@ -207,6 +232,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; @@ -256,51 +282,54 @@ class APIConnection : public APIServerConnection { } bool try_to_clear_buffer(bool log_out_of_space); - bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) override; + bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; - std::string get_client_combined_info() const { - if (this->client_info_ == this->client_peername_) { - // Before Hello message, both are the same (just IP:port) - return this->client_info_; - } - return this->client_info_ + " (" + this->client_peername_ + ")"; - } + std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); } // Buffer allocator methods for batch processing ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); 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, uint16_t message_type, APIConnection *conn, + 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 +#ifdef USE_ENTITY_ICON + msg.icon = entity->get_icon(); +#endif + 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; @@ -443,9 +472,6 @@ class APIConnection : public APIServerConnection { static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - // Helper function to get estimated message size for buffer pre-allocation - static uint16_t get_estimated_message_size(uint16_t message_type); - // Batch message method for ping requests static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); @@ -464,9 +490,8 @@ class APIConnection : public APIServerConnection { std::unique_ptr image_reader_; #endif - // Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned) - std::string client_info_; - std::string client_peername_; + // Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each) + ClientInfo client_info_; // Group 4: 4-byte types uint32_t last_traffic_; @@ -505,10 +530,10 @@ class APIConnection : public APIServerConnection { // Call operator - uses message_type to determine union type uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, - uint16_t message_type) const; + uint8_t message_type) const; // Manual cleanup method - must be called before destruction for string types - void cleanup(uint16_t message_type) { + void cleanup(uint8_t message_type) { #ifdef USE_EVENT if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) { delete data_.string_ptr; @@ -529,11 +554,12 @@ class APIConnection : public APIServerConnection { struct BatchItem { EntityBase *entity; // Entity pointer MessageCreator creator; // Function that creates the message when needed - uint16_t message_type; // Message type for overhead calculation + uint8_t message_type; // Message type for overhead calculation (max 255) + uint8_t estimated_size; // Estimated message size (max 255 bytes) // Constructor for creating BatchItem - BatchItem(EntityBase *entity, MessageCreator creator, uint16_t message_type) - : entity(entity), creator(std::move(creator)), message_type(message_type) {} + BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) + : entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {} }; std::vector items; @@ -559,9 +585,9 @@ class APIConnection : public APIServerConnection { } // Add item to the batch - void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type); + void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); // Add item to the front of the batch (for high priority messages like ping) - void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type); + void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); // Clear all items with proper cleanup void clear() { @@ -630,7 +656,7 @@ class APIConnection : public APIServerConnection { // to send in one go. This is the maximum size of a single packet // that can be sent over the network. // This is to avoid fragmentation of the packet. - static constexpr size_t MAX_PACKET_SIZE = 1390; // MTU + static constexpr size_t MAX_BATCH_PACKET_SIZE = 1390; // MTU bool schedule_batch_(); void process_batch_(); @@ -641,9 +667,9 @@ class APIConnection : public APIServerConnection { #ifdef HAS_PROTO_MESSAGE_DUMP // Helper to log a proto message from a MessageCreator object - void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint16_t message_type) { + void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint8_t message_type) { this->flags_.log_only_mode = true; - creator(entity, this, MAX_PACKET_SIZE, true, message_type); + creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type); this->flags_.log_only_mode = false; } @@ -654,7 +680,8 @@ class APIConnection : public APIServerConnection { #endif // Helper method to send a message either immediately or via batching - bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint16_t message_type) { + bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, + uint8_t estimated_size) { // Try to send immediately if: // 1. We should try to send immediately (should_try_send_immediately = true) // 2. Batch delay is 0 (user has opted in to immediate sending) @@ -662,7 +689,7 @@ class APIConnection : public APIServerConnection { if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 && this->helper_->can_write_without_blocking()) { // Now actually encode and send - if (creator(entity, this, MAX_PACKET_SIZE, true) && + if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { #ifdef HAS_PROTO_MESSAGE_DUMP // Log the message in verbose mode @@ -675,23 +702,25 @@ class APIConnection : public APIServerConnection { } // Fall back to scheduled batching - return this->schedule_message_(entity, creator, message_type); + return this->schedule_message_(entity, creator, message_type, estimated_size); } // Helper function to schedule a deferred message with known message type - bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) { - this->deferred_batch_.add_item(entity, std::move(creator), message_type); + bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { + this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size); return this->schedule_batch_(); } // Overload for function pointers (for info messages and current state reads) - bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { - return schedule_message_(entity, MessageCreator(function_ptr), message_type); + bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type, + uint8_t estimated_size) { + return schedule_message_(entity, MessageCreator(function_ptr), message_type, estimated_size); } // Helper function to schedule a high priority message at the front of the batch - bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { - this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type); + bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type, + uint8_t estimated_size) { + this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type, estimated_size); return this->schedule_batch_(); } }; diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 2f5acc3bfa..b1c9478e59 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -1,18 +1,28 @@ #include "api_frame_helper.h" #ifdef USE_API +#include "api_connection.h" // For ClientInfo struct #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "proto.h" -#include "api_pb2_size.h" #include #include namespace esphome { namespace api { -static const char *const TAG = "api.socket"; +static const char *const TAG = "api.frame_helper"; + +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) + +#ifdef HELPER_LOG_PACKETS +#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) +#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) +#else +#define LOG_PACKET_RECEIVED(buffer) ((void) 0) +#define LOG_PACKET_SENDING(data, len) ((void) 0) +#endif const char *api_error_to_str(APIError err) { // not using switch to ensure compiler doesn't try to build a big table out of it @@ -20,8 +30,6 @@ const char *api_error_to_str(APIError err) { return "OK"; } else if (err == APIError::WOULD_BLOCK) { return "WOULD_BLOCK"; - } else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { - return "BAD_HANDSHAKE_PACKET_LEN"; } else if (err == APIError::BAD_INDICATOR) { return "BAD_INDICATOR"; } else if (err == APIError::BAD_DATA_PACKET) { @@ -42,6 +50,14 @@ const char *api_error_to_str(APIError err) { return "SOCKET_READ_FAILED"; } else if (err == APIError::SOCKET_WRITE_FAILED) { return "SOCKET_WRITE_FAILED"; + } else if (err == APIError::OUT_OF_MEMORY) { + return "OUT_OF_MEMORY"; + } else if (err == APIError::CONNECTION_CLOSED) { + return "CONNECTION_CLOSED"; + } +#ifdef USE_API_NOISE + else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { + return "BAD_HANDSHAKE_PACKET_LEN"; } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) { return "HANDSHAKESTATE_READ_FAILED"; } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) { @@ -52,17 +68,14 @@ const char *api_error_to_str(APIError err) { return "CIPHERSTATE_DECRYPT_FAILED"; } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) { return "CIPHERSTATE_ENCRYPT_FAILED"; - } else if (err == APIError::OUT_OF_MEMORY) { - return "OUT_OF_MEMORY"; } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) { return "HANDSHAKESTATE_SETUP_FAILED"; } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { return "HANDSHAKESTATE_SPLIT_FAILED"; } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { return "BAD_HANDSHAKE_ERROR_BYTE"; - } else if (err == APIError::CONNECTION_CLOSED) { - return "CONNECTION_CLOSED"; } +#endif return "UNKNOWN"; } @@ -77,33 +90,55 @@ APIError APIFrameHelper::loop() { return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination } +// Common socket write error handling +APIError APIFrameHelper::handle_socket_write_error_() { + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return APIError::WOULD_BLOCK; + } + HELPER_LOG("Socket write failed with errno %d", errno); + this->state_ = State::FAILED; + return APIError::SOCKET_WRITE_FAILED; +} + // Helper method to buffer data from IOVs -void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { +void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, + uint16_t offset) { SendBuffer buffer; - buffer.data.reserve(total_write_len); + buffer.size = total_write_len - offset; + buffer.data = std::make_unique(buffer.size); + + uint16_t to_skip = offset; + uint16_t write_pos = 0; + for (int i = 0; i < iovcnt; i++) { - const uint8_t *data = reinterpret_cast(iov[i].iov_base); - buffer.data.insert(buffer.data.end(), data, data + iov[i].iov_len); + if (to_skip >= iov[i].iov_len) { + // Skip this entire segment + to_skip -= static_cast(iov[i].iov_len); + } else { + // Include this segment (partially or fully) + const uint8_t *src = reinterpret_cast(iov[i].iov_base) + to_skip; + uint16_t len = static_cast(iov[i].iov_len) - to_skip; + std::memcpy(buffer.data.get() + write_pos, src, len); + write_pos += len; + to_skip = 0; + } } this->tx_buf_.push_back(std::move(buffer)); } // This method writes data to socket or buffers it -APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { +APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { // Returns APIError::OK if successful (or would block, but data has been buffered) // Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED if (iovcnt == 0) return APIError::OK; // Nothing to do, success - uint16_t total_write_len = 0; - for (int i = 0; i < iovcnt; i++) { #ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Sending raw: %s", - format_hex_pretty(reinterpret_cast(iov[i].iov_base), iov[i].iov_len).c_str()); -#endif - total_write_len += static_cast(iov[i].iov_len); + for (int i = 0; i < iovcnt; i++) { + LOG_PACKET_SENDING(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); } +#endif // Try to send any existing buffered data first if there is any if (!this->tx_buf_.empty()) { @@ -116,7 +151,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { // If there is still data in the buffer, we can't send, buffer // the new data and return if (!this->tx_buf_.empty()) { - this->buffer_data_from_iov_(iov, iovcnt, total_write_len); + this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); return APIError::OK; // Success, data buffered } } @@ -125,37 +160,16 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { ssize_t sent = this->socket_->writev(iov, iovcnt); if (sent == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) { + APIError err = this->handle_socket_write_error_(); + if (err == APIError::WOULD_BLOCK) { // Socket would block, buffer the data - this->buffer_data_from_iov_(iov, iovcnt, total_write_len); + this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); return APIError::OK; // Success, data buffered } - // Socket error - ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno); - this->state_ = State::FAILED; - return APIError::SOCKET_WRITE_FAILED; // Socket write failed + return err; // Socket write failed } else if (static_cast(sent) < total_write_len) { // Partially sent, buffer the remaining data - SendBuffer buffer; - uint16_t to_consume = static_cast(sent); - uint16_t remaining = total_write_len - static_cast(sent); - - buffer.data.reserve(remaining); - - for (int i = 0; i < iovcnt; i++) { - if (to_consume >= iov[i].iov_len) { - // This segment was fully sent - to_consume -= static_cast(iov[i].iov_len); - } else { - // This segment was partially sent or not sent at all - const uint8_t *data = reinterpret_cast(iov[i].iov_base) + to_consume; - uint16_t len = static_cast(iov[i].iov_len) - to_consume; - buffer.data.insert(buffer.data.end(), data, data + len); - to_consume = 0; - } - } - - this->tx_buf_.push_back(std::move(buffer)); + this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast(sent)); } return APIError::OK; // Success, all data sent or buffered @@ -174,14 +188,7 @@ APIError APIFrameHelper::try_send_tx_buf_() { ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining()); if (sent == -1) { - if (errno != EWOULDBLOCK && errno != EAGAIN) { - // Real socket error (not just would block) - ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno); - this->state_ = State::FAILED; - return APIError::SOCKET_WRITE_FAILED; // Socket write failed - } - // Socket would block, we'll try again later - return APIError::WOULD_BLOCK; + return this->handle_socket_write_error_(); } else if (sent == 0) { // Nothing sent but not an error return APIError::WOULD_BLOCK; @@ -204,13 +211,13 @@ APIError APIFrameHelper::try_send_tx_buf_() { APIError APIFrameHelper::init_common_() { if (state_ != State::INITIALIZE || this->socket_ == nullptr) { - ESP_LOGVV(TAG, "%s: Bad state for init %d", this->info_.c_str(), (int) state_); + HELPER_LOG("Bad state for init %d", (int) state_); return APIError::BAD_STATE; } int err = this->socket_->setblocking(false); if (err != 0) { state_ = State::FAILED; - ESP_LOGVV(TAG, "%s: Setting nonblocking failed with errno %d", this->info_.c_str(), errno); + HELPER_LOG("Setting nonblocking failed with errno %d", errno); return APIError::TCP_NONBLOCKING_FAILED; } @@ -218,14 +225,12 @@ APIError APIFrameHelper::init_common_() { err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); if (err != 0) { state_ = State::FAILED; - ESP_LOGVV(TAG, "%s: Setting nodelay failed with errno %d", this->info_.c_str(), errno); + HELPER_LOG("Setting nodelay failed with errno %d", errno); return APIError::TCP_NODELAY_FAILED; } return APIError::OK; } -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__) - APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) { if (received == -1) { if (errno == EWOULDBLOCK || errno == EAGAIN) { @@ -241,839 +246,6 @@ APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) { } return APIError::OK; } -// uncomment to log raw packets -//#define HELPER_LOG_PACKETS - -#ifdef USE_API_NOISE -static const char *const PROLOGUE_INIT = "NoiseAPIInit"; - -/// Convert a noise error code to a readable error -std::string noise_err_to_str(int err) { - if (err == NOISE_ERROR_NO_MEMORY) - return "NO_MEMORY"; - if (err == NOISE_ERROR_UNKNOWN_ID) - return "UNKNOWN_ID"; - if (err == NOISE_ERROR_UNKNOWN_NAME) - return "UNKNOWN_NAME"; - if (err == NOISE_ERROR_MAC_FAILURE) - return "MAC_FAILURE"; - if (err == NOISE_ERROR_NOT_APPLICABLE) - return "NOT_APPLICABLE"; - if (err == NOISE_ERROR_SYSTEM) - return "SYSTEM"; - if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) - return "REMOTE_KEY_REQUIRED"; - if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) - return "LOCAL_KEY_REQUIRED"; - if (err == NOISE_ERROR_PSK_REQUIRED) - return "PSK_REQUIRED"; - if (err == NOISE_ERROR_INVALID_LENGTH) - return "INVALID_LENGTH"; - if (err == NOISE_ERROR_INVALID_PARAM) - return "INVALID_PARAM"; - if (err == NOISE_ERROR_INVALID_STATE) - return "INVALID_STATE"; - if (err == NOISE_ERROR_INVALID_NONCE) - return "INVALID_NONCE"; - if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) - return "INVALID_PRIVATE_KEY"; - if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) - return "INVALID_PUBLIC_KEY"; - if (err == NOISE_ERROR_INVALID_FORMAT) - return "INVALID_FORMAT"; - if (err == NOISE_ERROR_INVALID_SIGNATURE) - return "INVALID_SIGNATURE"; - return to_string(err); -} - -/// Initialize the frame helper, returns OK if successful. -APIError APINoiseFrameHelper::init() { - APIError err = init_common_(); - if (err != APIError::OK) { - return err; - } - - // init prologue - prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT)); - - state_ = State::CLIENT_HELLO; - return APIError::OK; -} -/// Run through handshake messages (if in that phase) -APIError APINoiseFrameHelper::loop() { - // During handshake phase, process as many actions as possible until we can't progress - // socket_->ready() stays true until next main loop, but state_action() will return - // WOULD_BLOCK when no more data is available to read - while (state_ != State::DATA && this->socket_->ready()) { - APIError err = state_action_(); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - return err; - } - if (err == APIError::WOULD_BLOCK) { - break; - } - } - - // Use base class implementation for buffer sending - return APIFrameHelper::loop(); -} - -/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter - * - * @param frame: The struct to hold the frame information in. - * msg_start: points to the start of the payload - this pointer is only valid until the next - * try_receive_raw_ call - * - * @return 0 if a full packet is in rx_buf_ - * @return -1 if error, check errno. - * - * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. - * errno ENOMEM: Not enough memory for reading packet. - * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. - * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. - */ -APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { - if (frame == nullptr) { - HELPER_LOG("Bad argument for try_read_frame_"); - return APIError::BAD_ARG; - } - - // read header - if (rx_header_buf_len_ < 3) { - // no header information yet - uint8_t to_read = 3 - rx_header_buf_len_; - ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); - APIError err = handle_socket_read_result_(received); - if (err != APIError::OK) { - return err; - } - rx_header_buf_len_ += static_cast(received); - if (static_cast(received) != to_read) { - // not a full read - return APIError::WOULD_BLOCK; - } - - if (rx_header_buf_[0] != 0x01) { - state_ = State::FAILED; - HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); - return APIError::BAD_INDICATOR; - } - // header reading done - } - - // read body - uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; - - if (state_ != State::DATA && msg_size > 128) { - // for handshake message only permit up to 128 bytes - state_ = State::FAILED; - HELPER_LOG("Bad packet len for handshake: %d", msg_size); - return APIError::BAD_HANDSHAKE_PACKET_LEN; - } - - // reserve space for body - if (rx_buf_.size() != msg_size) { - rx_buf_.resize(msg_size); - } - - if (rx_buf_len_ < msg_size) { - // more data to read - uint16_t to_read = msg_size - rx_buf_len_; - ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); - APIError err = handle_socket_read_result_(received); - if (err != APIError::OK) { - return err; - } - rx_buf_len_ += static_cast(received); - if (static_cast(received) != to_read) { - // not all read - return APIError::WOULD_BLOCK; - } - } - - // uncomment for even more debugging -#ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str()); -#endif - frame->msg = std::move(rx_buf_); - // consume msg - rx_buf_ = {}; - rx_buf_len_ = 0; - rx_header_buf_len_ = 0; - return APIError::OK; -} - -/** To be called from read/write methods. - * - * This method runs through the internal handshake methods, if in that state. - * - * If the handshake is still active when this method returns and a read/write can't take place at - * the moment, returns WOULD_BLOCK. - * If an error occurred, returns that error. Only returns OK if the transport is ready for data - * traffic. - */ -APIError APINoiseFrameHelper::state_action_() { - int err; - APIError aerr; - if (state_ == State::INITIALIZE) { - HELPER_LOG("Bad state for method: %d", (int) state_); - return APIError::BAD_STATE; - } - if (state_ == State::CLIENT_HELLO) { - // waiting for client hello - ParsedFrame frame; - aerr = try_read_frame_(&frame); - if (aerr == APIError::BAD_INDICATOR) { - send_explicit_handshake_reject_("Bad indicator byte"); - return aerr; - } - if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { - send_explicit_handshake_reject_("Bad handshake packet len"); - return aerr; - } - if (aerr != APIError::OK) - return aerr; - // ignore contents, may be used in future for flags - // Reserve space for: existing prologue + 2 size bytes + frame data - prologue_.reserve(prologue_.size() + 2 + frame.msg.size()); - prologue_.push_back((uint8_t) (frame.msg.size() >> 8)); - prologue_.push_back((uint8_t) frame.msg.size()); - prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end()); - - state_ = State::SERVER_HELLO; - } - if (state_ == State::SERVER_HELLO) { - // send server hello - const std::string &name = App.get_name(); - const std::string &mac = get_mac_address(); - - std::vector msg; - // Reserve space for: 1 byte proto + name + null + mac + null - msg.reserve(1 + name.size() + 1 + mac.size() + 1); - - // chosen proto - msg.push_back(0x01); - - // node name, terminated by null byte - const uint8_t *name_ptr = reinterpret_cast(name.c_str()); - msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1); - // node mac, terminated by null byte - const uint8_t *mac_ptr = reinterpret_cast(mac.c_str()); - msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1); - - aerr = write_frame_(msg.data(), msg.size()); - if (aerr != APIError::OK) - return aerr; - - // start handshake - aerr = init_handshake_(); - if (aerr != APIError::OK) - return aerr; - - state_ = State::HANDSHAKE; - } - if (state_ == State::HANDSHAKE) { - int action = noise_handshakestate_get_action(handshake_); - if (action == NOISE_ACTION_READ_MESSAGE) { - // waiting for handshake msg - ParsedFrame frame; - aerr = try_read_frame_(&frame); - if (aerr == APIError::BAD_INDICATOR) { - send_explicit_handshake_reject_("Bad indicator byte"); - return aerr; - } - if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { - send_explicit_handshake_reject_("Bad handshake packet len"); - return aerr; - } - if (aerr != APIError::OK) - return aerr; - - if (frame.msg.empty()) { - send_explicit_handshake_reject_("Empty handshake message"); - return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } else if (frame.msg[0] != 0x00) { - HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]); - send_explicit_handshake_reject_("Bad handshake error byte"); - return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } - - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1); - err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str()); - if (err == NOISE_ERROR_MAC_FAILURE) { - send_explicit_handshake_reject_("Handshake MAC failure"); - } else { - send_explicit_handshake_reject_("Handshake error"); - } - return APIError::HANDSHAKESTATE_READ_FAILED; - } - - aerr = check_handshake_finished_(); - if (aerr != APIError::OK) - return aerr; - } else if (action == NOISE_ACTION_WRITE_MESSAGE) { - uint8_t buffer[65]; - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); - - err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str()); - return APIError::HANDSHAKESTATE_WRITE_FAILED; - } - buffer[0] = 0x00; // success - - aerr = write_frame_(buffer, mbuf.size + 1); - if (aerr != APIError::OK) - return aerr; - aerr = check_handshake_finished_(); - if (aerr != APIError::OK) - return aerr; - } else { - // bad state for action - state_ = State::FAILED; - HELPER_LOG("Bad action for handshake: %d", action); - return APIError::HANDSHAKESTATE_BAD_STATE; - } - } - if (state_ == State::CLOSED || state_ == State::FAILED) { - return APIError::BAD_STATE; - } - return APIError::OK; -} -void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { - std::vector data; - data.resize(reason.length() + 1); - data[0] = 0x01; // failure - - // Copy error message in bulk - if (!reason.empty()) { - std::memcpy(data.data() + 1, reason.c_str(), reason.length()); - } - - // temporarily remove failed state - auto orig_state = state_; - state_ = State::EXPLICIT_REJECT; - write_frame_(data.data(), data.size()); - state_ = orig_state; -} -APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { - int err; - APIError aerr; - aerr = state_action_(); - if (aerr != APIError::OK) { - return aerr; - } - - if (state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } - - ParsedFrame frame; - aerr = try_read_frame_(&frame); - if (aerr != APIError::OK) - return aerr; - - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size()); - err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str()); - return APIError::CIPHERSTATE_DECRYPT_FAILED; - } - - uint16_t msg_size = mbuf.size; - uint8_t *msg_data = frame.msg.data(); - if (msg_size < 4) { - state_ = State::FAILED; - HELPER_LOG("Bad data packet: size %d too short", msg_size); - return APIError::BAD_DATA_PACKET; - } - - uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; - uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; - if (data_len > msg_size - 4) { - state_ = State::FAILED; - HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); - return APIError::BAD_DATA_PACKET; - } - - buffer->container = std::move(frame.msg); - buffer->data_offset = 4; - buffer->data_len = data_len; - buffer->type = type; - return APIError::OK; -} -APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { - // Resize to include MAC space (required for Noise encryption) - buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); - PacketInfo packet{type, 0, - static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; - return write_protobuf_packets(buffer, std::span(&packet, 1)); -} - -APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { - APIError aerr = state_action_(); - if (aerr != APIError::OK) { - return aerr; - } - - if (state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } - - if (packets.empty()) { - return APIError::OK; - } - - std::vector *raw_buffer = buffer.get_buffer(); - uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer - - this->reusable_iovs_.clear(); - this->reusable_iovs_.reserve(packets.size()); - - // We need to encrypt each packet in place - for (const auto &packet : packets) { - // The buffer already has padding at offset - uint8_t *buf_start = buffer_data + packet.offset; - - // Write noise header - buf_start[0] = 0x01; // indicator - // buf_start[1], buf_start[2] to be set after encryption - - // Write message header (to be encrypted) - const uint8_t msg_offset = 3; - buf_start[msg_offset] = static_cast(packet.message_type >> 8); // type high byte - buf_start[msg_offset + 1] = static_cast(packet.message_type); // type low byte - buf_start[msg_offset + 2] = static_cast(packet.payload_size >> 8); // data_len high byte - buf_start[msg_offset + 3] = static_cast(packet.payload_size); // data_len low byte - // payload data is already in the buffer starting at offset + 7 - - // Make sure we have space for MAC - // The buffer should already have been sized appropriately - - // Encrypt the message in place - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size, - 4 + packet.payload_size + frame_footer_size_); - - int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str()); - return APIError::CIPHERSTATE_ENCRYPT_FAILED; - } - - // Fill in the encrypted size - buf_start[1] = static_cast(mbuf.size >> 8); - buf_start[2] = static_cast(mbuf.size); - - // Add iovec for this encrypted packet - this->reusable_iovs_.push_back( - {buf_start, static_cast(3 + mbuf.size)}); // indicator + size + encrypted data - } - - // Send all encrypted packets in one writev call - return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size()); -} - -APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { - uint8_t header[3]; - header[0] = 0x01; // indicator - header[1] = (uint8_t) (len >> 8); - header[2] = (uint8_t) len; - - struct iovec iov[2]; - iov[0].iov_base = header; - iov[0].iov_len = 3; - if (len == 0) { - return this->write_raw_(iov, 1); - } - iov[1].iov_base = const_cast(data); - iov[1].iov_len = len; - - return this->write_raw_(iov, 2); -} - -/** Initiate the data structures for the handshake. - * - * @return 0 on success, -1 on error (check errno) - */ -APIError APINoiseFrameHelper::init_handshake_() { - int err; - memset(&nid_, 0, sizeof(nid_)); - // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; - // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto)); - nid_.pattern_id = NOISE_PATTERN_NN; - nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY; - nid_.dh_id = NOISE_DH_CURVE25519; - nid_.prefix_id = NOISE_PREFIX_STANDARD; - nid_.hybrid_id = NOISE_DH_NONE; - nid_.hash_id = NOISE_HASH_SHA256; - nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; - - err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str()); - return APIError::HANDSHAKESTATE_SETUP_FAILED; - } - - const auto &psk = ctx_->get_psk(); - err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str()); - return APIError::HANDSHAKESTATE_SETUP_FAILED; - } - - err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str()); - return APIError::HANDSHAKESTATE_SETUP_FAILED; - } - // set_prologue copies it into handshakestate, so we can get rid of it now - prologue_ = {}; - - err = noise_handshakestate_start(handshake_); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str()); - return APIError::HANDSHAKESTATE_SETUP_FAILED; - } - return APIError::OK; -} - -APIError APINoiseFrameHelper::check_handshake_finished_() { - assert(state_ == State::HANDSHAKE); - - int action = noise_handshakestate_get_action(handshake_); - if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) - return APIError::OK; - if (action != NOISE_ACTION_SPLIT) { - state_ = State::FAILED; - HELPER_LOG("Bad action for handshake: %d", action); - return APIError::HANDSHAKESTATE_BAD_STATE; - } - int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); - if (err != 0) { - state_ = State::FAILED; - HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str()); - return APIError::HANDSHAKESTATE_SPLIT_FAILED; - } - - frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_); - - HELPER_LOG("Handshake complete!"); - noise_handshakestate_free(handshake_); - handshake_ = nullptr; - state_ = State::DATA; - return APIError::OK; -} - -APINoiseFrameHelper::~APINoiseFrameHelper() { - if (handshake_ != nullptr) { - noise_handshakestate_free(handshake_); - handshake_ = nullptr; - } - if (send_cipher_ != nullptr) { - noise_cipherstate_free(send_cipher_); - send_cipher_ = nullptr; - } - if (recv_cipher_ != nullptr) { - noise_cipherstate_free(recv_cipher_); - recv_cipher_ = nullptr; - } -} - -extern "C" { -// declare how noise generates random bytes (here with a good HWRNG based on the RF system) -void noise_rand_bytes(void *output, size_t len) { - if (!esphome::random_bytes(reinterpret_cast(output), len)) { - ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting"); - arch_restart(); - } -} -} - -#endif // USE_API_NOISE - -#ifdef USE_API_PLAINTEXT - -/// Initialize the frame helper, returns OK if successful. -APIError APIPlaintextFrameHelper::init() { - APIError err = init_common_(); - if (err != APIError::OK) { - return err; - } - - state_ = State::DATA; - return APIError::OK; -} -APIError APIPlaintextFrameHelper::loop() { - if (state_ != State::DATA) { - return APIError::BAD_STATE; - } - // Use base class implementation for buffer sending - return APIFrameHelper::loop(); -} - -/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter - * - * @param frame: The struct to hold the frame information in. - * msg: store the parsed frame in that struct - * - * @return See APIError - * - * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. - */ -APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { - if (frame == nullptr) { - HELPER_LOG("Bad argument for try_read_frame_"); - return APIError::BAD_ARG; - } - - // read header - while (!rx_header_parsed_) { - // Now that we know when the socket is ready, we can read up to 3 bytes - // into the rx_header_buf_ before we have to switch back to reading - // one byte at a time to ensure we don't read past the message and - // into the next one. - - // Read directly into rx_header_buf_ at the current position - // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time - ssize_t received = - this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1); - APIError err = handle_socket_read_result_(received); - if (err != APIError::OK) { - return err; - } - - // If this was the first read, validate the indicator byte - if (rx_header_buf_pos_ == 0 && received > 0) { - if (rx_header_buf_[0] != 0x00) { - state_ = State::FAILED; - HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); - return APIError::BAD_INDICATOR; - } - } - - rx_header_buf_pos_ += received; - - // Check for buffer overflow - if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) { - state_ = State::FAILED; - HELPER_LOG("Header buffer overflow"); - return APIError::BAD_DATA_PACKET; - } - - // Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse - if (rx_header_buf_pos_ < 3) { - continue; - } - - // At this point, we have at least 3 bytes total: - // - Validated indicator byte (0x00) stored at position 0 - // - At least 2 bytes in the buffer for the varints - // Buffer layout: - // [0]: indicator byte (0x00) - // [1-3]: Message size varint (variable length) - // - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535) - // - 3 bytes allows up to 2097151, ensuring we support at least as much as noise - // [2-5]: Message type varint (variable length) - // We now attempt to parse both varints. If either is incomplete, - // we'll continue reading more bytes. - - // Skip indicator byte at position 0 - uint8_t varint_pos = 1; - uint32_t consumed = 0; - - auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); - if (!msg_size_varint.has_value()) { - // not enough data there yet - continue; - } - - if (msg_size_varint->as_uint32() > std::numeric_limits::max()) { - state_ = State::FAILED; - HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(), - std::numeric_limits::max()); - return APIError::BAD_DATA_PACKET; - } - rx_header_parsed_len_ = msg_size_varint->as_uint16(); - - // Move to next varint position - varint_pos += consumed; - - auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); - if (!msg_type_varint.has_value()) { - // not enough data there yet - continue; - } - if (msg_type_varint->as_uint32() > std::numeric_limits::max()) { - state_ = State::FAILED; - HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(), - std::numeric_limits::max()); - return APIError::BAD_DATA_PACKET; - } - rx_header_parsed_type_ = msg_type_varint->as_uint16(); - rx_header_parsed_ = true; - } - // header reading done - - // reserve space for body - if (rx_buf_.size() != rx_header_parsed_len_) { - rx_buf_.resize(rx_header_parsed_len_); - } - - if (rx_buf_len_ < rx_header_parsed_len_) { - // more data to read - uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_; - ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); - APIError err = handle_socket_read_result_(received); - if (err != APIError::OK) { - return err; - } - rx_buf_len_ += static_cast(received); - if (static_cast(received) != to_read) { - // not all read - return APIError::WOULD_BLOCK; - } - } - - // uncomment for even more debugging -#ifdef HELPER_LOG_PACKETS - ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(rx_buf_).c_str()); -#endif - frame->msg = std::move(rx_buf_); - // consume msg - rx_buf_ = {}; - rx_buf_len_ = 0; - rx_header_buf_pos_ = 0; - rx_header_parsed_ = false; - return APIError::OK; -} -APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { - APIError aerr; - - if (state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } - - ParsedFrame frame; - aerr = try_read_frame_(&frame); - if (aerr != APIError::OK) { - if (aerr == APIError::BAD_INDICATOR) { - // Make sure to tell the remote that we don't - // understand the indicator byte so it knows - // we do not support it. - struct iovec iov[1]; - // The \x00 first byte is the marker for plaintext. - // - // The remote will know how to handle the indicator byte, - // but it likely won't understand the rest of the message. - // - // We must send at least 3 bytes to be read, so we add - // a message after the indicator byte to ensures its long - // enough and can aid in debugging. - const char msg[] = "\x00" - "Bad indicator byte"; - iov[0].iov_base = (void *) msg; - iov[0].iov_len = 19; - this->write_raw_(iov, 1); - } - return aerr; - } - - buffer->container = std::move(frame.msg); - buffer->data_offset = 0; - buffer->data_len = rx_header_parsed_len_; - buffer->type = rx_header_parsed_type_; - return APIError::OK; -} -APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { - PacketInfo packet{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_)}; - return write_protobuf_packets(buffer, std::span(&packet, 1)); -} - -APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { - if (state_ != State::DATA) { - return APIError::BAD_STATE; - } - - if (packets.empty()) { - return APIError::OK; - } - - std::vector *raw_buffer = buffer.get_buffer(); - uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer - - this->reusable_iovs_.clear(); - this->reusable_iovs_.reserve(packets.size()); - - for (const auto &packet : packets) { - // Calculate varint sizes for header layout - uint8_t size_varint_len = api::ProtoSize::varint(static_cast(packet.payload_size)); - uint8_t type_varint_len = api::ProtoSize::varint(static_cast(packet.message_type)); - uint8_t total_header_len = 1 + size_varint_len + type_varint_len; - - // Calculate where to start writing the header - // The header starts at the latest possible position to minimize unused padding - // - // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3 - // [0-2] - Unused padding - // [3] - 0x00 indicator byte - // [4] - Payload size varint (1 byte, for sizes 0-127) - // [5] - Message type varint (1 byte, for types 0-127) - // [6...] - Actual payload data - // - // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2 - // [0-1] - Unused padding - // [2] - 0x00 indicator byte - // [3-4] - Payload size varint (2 bytes, for sizes 128-16383) - // [5] - Message type varint (1 byte, for types 0-127) - // [6...] - Actual payload data - // - // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0 - // [0] - 0x00 indicator byte - // [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151) - // [4-5] - Message type varint (2 bytes, for types 128-32767) - // [6...] - Actual payload data - // - // The message starts at offset + frame_header_padding_ - // So we write the header starting at offset + frame_header_padding_ - total_header_len - uint8_t *buf_start = buffer_data + packet.offset; - uint32_t header_offset = frame_header_padding_ - total_header_len; - - // Write the plaintext header - buf_start[header_offset] = 0x00; // indicator - - // Encode varints directly into buffer - ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); - ProtoVarInt(packet.message_type) - .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); - - // Add iovec for this packet (header + payload) - this->reusable_iovs_.push_back( - {buf_start + header_offset, static_cast(total_header_len + packet.payload_size)}); - } - - // Send all packets in one writev call - return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size()); -} - -#endif // USE_API_PLAINTEXT } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index eae83a3484..231a3366ce 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -8,17 +8,19 @@ #include "esphome/core/defines.h" #ifdef USE_API -#ifdef USE_API_NOISE -#include "noise/protocol.h" -#endif - -#include "api_noise_context.h" #include "esphome/components/socket/socket.h" #include "esphome/core/application.h" +#include "esphome/core/log.h" namespace esphome { namespace api { +// uncomment to log raw packets +//#define HELPER_LOG_PACKETS + +// Forward declaration +struct ClientInfo; + class ProtoWriteBuffer; struct ReadPacketBuffer { @@ -30,19 +32,16 @@ struct ReadPacketBuffer { // Packed packet info structure to minimize memory usage struct PacketInfo { - uint16_t message_type; // 2 bytes - uint16_t offset; // 2 bytes (sufficient for packet size ~1460 bytes) - uint16_t payload_size; // 2 bytes (up to 65535 bytes) - uint16_t padding; // 2 byte (for alignment) + uint16_t offset; // Offset in buffer where message starts + uint16_t payload_size; // Size of the message payload + uint8_t message_type; // Message type (0-255) - PacketInfo(uint16_t type, uint16_t off, uint16_t size) - : message_type(type), offset(off), payload_size(size), padding(0) {} + PacketInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {} }; enum class APIError : uint16_t { OK = 0, WOULD_BLOCK = 1001, - BAD_HANDSHAKE_PACKET_LEN = 1002, BAD_INDICATOR = 1003, BAD_DATA_PACKET = 1004, TCP_NODELAY_FAILED = 1005, @@ -53,16 +52,19 @@ enum class APIError : uint16_t { BAD_ARG = 1010, SOCKET_READ_FAILED = 1011, SOCKET_WRITE_FAILED = 1012, + OUT_OF_MEMORY = 1018, + CONNECTION_CLOSED = 1022, +#ifdef USE_API_NOISE + BAD_HANDSHAKE_PACKET_LEN = 1002, HANDSHAKESTATE_READ_FAILED = 1013, HANDSHAKESTATE_WRITE_FAILED = 1014, HANDSHAKESTATE_BAD_STATE = 1015, CIPHERSTATE_DECRYPT_FAILED = 1016, CIPHERSTATE_ENCRYPT_FAILED = 1017, - OUT_OF_MEMORY = 1018, HANDSHAKESTATE_SETUP_FAILED = 1019, HANDSHAKESTATE_SPLIT_FAILED = 1020, BAD_HANDSHAKE_ERROR_BYTE = 1021, - CONNECTION_CLOSED = 1022, +#endif }; const char *api_error_to_str(APIError err); @@ -70,7 +72,8 @@ const char *api_error_to_str(APIError err); class APIFrameHelper { public: APIFrameHelper() = default; - explicit APIFrameHelper(std::unique_ptr socket) : socket_owned_(std::move(socket)) { + explicit APIFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) + : socket_owned_(std::move(socket)), client_info_(client_info) { socket_ = socket_owned_.get(); } virtual ~APIFrameHelper() = default; @@ -96,9 +99,7 @@ class APIFrameHelper { } return APIError::OK; } - // Give this helper a name for logging - void set_log_info(std::string info) { info_ = std::move(info); } - virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0; + virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; // Write multiple protobuf packets in a single operation // packets contains (message_type, offset, length) for each message in the buffer // The buffer contains all messages with appropriate padding before each @@ -111,29 +112,28 @@ class APIFrameHelper { bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } protected: - // Struct for holding parsed frame data - struct ParsedFrame { - std::vector msg; - }; - // Buffer containing data to be sent struct SendBuffer { - std::vector data; - uint16_t offset{0}; // Current offset within the buffer (uint16_t to reduce memory usage) + std::unique_ptr data; + uint16_t size{0}; // Total size of the buffer + uint16_t offset{0}; // Current offset within the buffer // Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes - uint16_t remaining() const { return static_cast(data.size()) - offset; } - const uint8_t *current_data() const { return data.data() + offset; } + uint16_t remaining() const { return size - offset; } + const uint8_t *current_data() const { return data.get() + offset; } }; // Common implementation for writing raw data to socket - APIError write_raw_(const struct iovec *iov, int iovcnt); + APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len); // Try to send data from the tx buffer APIError try_send_tx_buf_(); // Helper method to buffer data from IOVs - void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len); + void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset); + + // Common socket write error handling + APIError handle_socket_write_error_(); template APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); @@ -163,10 +163,13 @@ class APIFrameHelper { // Containers (size varies, but typically 12+ bytes on 32-bit) std::deque tx_buf_; - std::string info_; std::vector reusable_iovs_; std::vector rx_buf_; + // Pointer to client info (4 bytes on 32-bit) + // Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance. + const ClientInfo *client_info_{nullptr}; + // Group smaller types together uint16_t rx_buf_len_ = 0; State state_{State::INITIALIZE}; @@ -181,105 +184,7 @@ class APIFrameHelper { APIError handle_socket_read_result_(ssize_t received); }; -#ifdef USE_API_NOISE -class APINoiseFrameHelper : public APIFrameHelper { - public: - APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx) - : APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) { - // Noise header structure: - // Pos 0: indicator (0x01) - // Pos 1-2: encrypted payload size (16-bit big-endian) - // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) - // Pos 7+: actual payload data - frame_header_padding_ = 7; - } - ~APINoiseFrameHelper() override; - APIError init() override; - APIError loop() override; - APIError read_packet(ReadPacketBuffer *buffer) override; - APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; - APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; - // Get the frame header padding required by this protocol - uint8_t frame_header_padding() override { return frame_header_padding_; } - // Get the frame footer size required by this protocol - uint8_t frame_footer_size() override { return frame_footer_size_; } - - protected: - APIError state_action_(); - APIError try_read_frame_(ParsedFrame *frame); - APIError write_frame_(const uint8_t *data, uint16_t len); - APIError init_handshake_(); - APIError check_handshake_finished_(); - void send_explicit_handshake_reject_(const std::string &reason); - - // Pointers first (4 bytes each) - NoiseHandshakeState *handshake_{nullptr}; - NoiseCipherState *send_cipher_{nullptr}; - NoiseCipherState *recv_cipher_{nullptr}; - - // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) - std::shared_ptr ctx_; - - // Vector (12 bytes on 32-bit) - std::vector prologue_; - - // NoiseProtocolId (size depends on implementation) - NoiseProtocolId nid_; - - // Group small types together - // Fixed-size header buffer for noise protocol: - // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) - // Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase - uint8_t rx_header_buf_[3]; - uint8_t rx_header_buf_len_ = 0; - // 4 bytes total, no padding -}; -#endif // USE_API_NOISE - -#ifdef USE_API_PLAINTEXT -class APIPlaintextFrameHelper : public APIFrameHelper { - public: - APIPlaintextFrameHelper(std::unique_ptr socket) : APIFrameHelper(std::move(socket)) { - // Plaintext header structure (worst case): - // Pos 0: indicator (0x00) - // Pos 1-3: payload size varint (up to 3 bytes) - // Pos 4-5: message type varint (up to 2 bytes) - // Pos 6+: actual payload data - frame_header_padding_ = 6; - } - ~APIPlaintextFrameHelper() override = default; - APIError init() override; - APIError loop() override; - APIError read_packet(ReadPacketBuffer *buffer) override; - APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; - APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; - uint8_t frame_header_padding() override { return frame_header_padding_; } - // Get the frame footer size required by this protocol - uint8_t frame_footer_size() override { return frame_footer_size_; } - - protected: - APIError try_read_frame_(ParsedFrame *frame); - - // Group 2-byte aligned types - uint16_t rx_header_parsed_type_ = 0; - uint16_t rx_header_parsed_len_ = 0; - - // Group 1-byte types together - // Fixed-size header buffer for plaintext protocol: - // We now store the indicator byte + the two varints. - // To match noise protocol's maximum message size (UINT16_MAX = 65535), we need: - // 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint - // - // While varints could theoretically be up to 10 bytes each for 64-bit values, - // attempting to process messages with headers that large would likely crash the - // ESP32 due to memory constraints. - uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type) - uint8_t rx_header_buf_pos_ = 0; - bool rx_header_parsed_ = false; - // 8 bytes total, no padding needed -}; -#endif - } // namespace api } // namespace esphome -#endif + +#endif // USE_API diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp new file mode 100644 index 0000000000..3c2c9e059e --- /dev/null +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -0,0 +1,577 @@ +#include "api_frame_helper_noise.h" +#ifdef USE_API +#ifdef USE_API_NOISE +#include "api_connection.h" // For ClientInfo struct +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "proto.h" +#include +#include + +namespace esphome { +namespace api { + +static const char *const TAG = "api.noise"; +static const char *const PROLOGUE_INIT = "NoiseAPIInit"; + +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) + +#ifdef HELPER_LOG_PACKETS +#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) +#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) +#else +#define LOG_PACKET_RECEIVED(buffer) ((void) 0) +#define LOG_PACKET_SENDING(data, len) ((void) 0) +#endif + +/// Convert a noise error code to a readable error +std::string noise_err_to_str(int err) { + if (err == NOISE_ERROR_NO_MEMORY) + return "NO_MEMORY"; + if (err == NOISE_ERROR_UNKNOWN_ID) + return "UNKNOWN_ID"; + if (err == NOISE_ERROR_UNKNOWN_NAME) + return "UNKNOWN_NAME"; + if (err == NOISE_ERROR_MAC_FAILURE) + return "MAC_FAILURE"; + if (err == NOISE_ERROR_NOT_APPLICABLE) + return "NOT_APPLICABLE"; + if (err == NOISE_ERROR_SYSTEM) + return "SYSTEM"; + if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) + return "REMOTE_KEY_REQUIRED"; + if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) + return "LOCAL_KEY_REQUIRED"; + if (err == NOISE_ERROR_PSK_REQUIRED) + return "PSK_REQUIRED"; + if (err == NOISE_ERROR_INVALID_LENGTH) + return "INVALID_LENGTH"; + if (err == NOISE_ERROR_INVALID_PARAM) + return "INVALID_PARAM"; + if (err == NOISE_ERROR_INVALID_STATE) + return "INVALID_STATE"; + if (err == NOISE_ERROR_INVALID_NONCE) + return "INVALID_NONCE"; + if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) + return "INVALID_PRIVATE_KEY"; + if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) + return "INVALID_PUBLIC_KEY"; + if (err == NOISE_ERROR_INVALID_FORMAT) + return "INVALID_FORMAT"; + if (err == NOISE_ERROR_INVALID_SIGNATURE) + return "INVALID_SIGNATURE"; + return to_string(err); +} + +/// Initialize the frame helper, returns OK if successful. +APIError APINoiseFrameHelper::init() { + APIError err = init_common_(); + if (err != APIError::OK) { + return err; + } + + // init prologue + prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT)); + + state_ = State::CLIENT_HELLO; + return APIError::OK; +} +// Helper for handling handshake frame errors +APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) { + if (aerr == APIError::BAD_INDICATOR) { + send_explicit_handshake_reject_("Bad indicator byte"); + } else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { + send_explicit_handshake_reject_("Bad handshake packet len"); + } + return aerr; +} + +// Helper for handling noise library errors +APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) { + if (err != 0) { + state_ = State::FAILED; + HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str()); + return api_err; + } + return APIError::OK; +} + +/// Run through handshake messages (if in that phase) +APIError APINoiseFrameHelper::loop() { + // During handshake phase, process as many actions as possible until we can't progress + // socket_->ready() stays true until next main loop, but state_action() will return + // WOULD_BLOCK when no more data is available to read + while (state_ != State::DATA && this->socket_->ready()) { + APIError err = state_action_(); + if (err == APIError::WOULD_BLOCK) { + break; + } + if (err != APIError::OK) { + return err; + } + } + + // Use base class implementation for buffer sending + return APIFrameHelper::loop(); +} + +/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter + * + * @param frame: The struct to hold the frame information in. + * msg_start: points to the start of the payload - this pointer is only valid until the next + * try_receive_raw_ call + * + * @return 0 if a full packet is in rx_buf_ + * @return -1 if error, check errno. + * + * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. + * errno ENOMEM: Not enough memory for reading packet. + * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. + * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. + */ +APIError APINoiseFrameHelper::try_read_frame_(std::vector *frame) { + if (frame == nullptr) { + HELPER_LOG("Bad argument for try_read_frame_"); + return APIError::BAD_ARG; + } + + // read header + if (rx_header_buf_len_ < 3) { + // no header information yet + uint8_t to_read = 3 - rx_header_buf_len_; + ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; + } + rx_header_buf_len_ += static_cast(received); + if (static_cast(received) != to_read) { + // not a full read + return APIError::WOULD_BLOCK; + } + + if (rx_header_buf_[0] != 0x01) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); + return APIError::BAD_INDICATOR; + } + // header reading done + } + + // read body + uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; + + if (state_ != State::DATA && msg_size > 128) { + // for handshake message only permit up to 128 bytes + state_ = State::FAILED; + HELPER_LOG("Bad packet len for handshake: %d", msg_size); + return APIError::BAD_HANDSHAKE_PACKET_LEN; + } + + // reserve space for body + if (rx_buf_.size() != msg_size) { + rx_buf_.resize(msg_size); + } + + if (rx_buf_len_ < msg_size) { + // more data to read + uint16_t to_read = msg_size - rx_buf_len_; + ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; + } + rx_buf_len_ += static_cast(received); + if (static_cast(received) != to_read) { + // not all read + return APIError::WOULD_BLOCK; + } + } + + LOG_PACKET_RECEIVED(rx_buf_); + *frame = std::move(rx_buf_); + // consume msg + rx_buf_ = {}; + rx_buf_len_ = 0; + rx_header_buf_len_ = 0; + return APIError::OK; +} + +/** To be called from read/write methods. + * + * This method runs through the internal handshake methods, if in that state. + * + * If the handshake is still active when this method returns and a read/write can't take place at + * the moment, returns WOULD_BLOCK. + * If an error occurred, returns that error. Only returns OK if the transport is ready for data + * traffic. + */ +APIError APINoiseFrameHelper::state_action_() { + int err; + APIError aerr; + if (state_ == State::INITIALIZE) { + HELPER_LOG("Bad state for method: %d", (int) state_); + return APIError::BAD_STATE; + } + if (state_ == State::CLIENT_HELLO) { + // waiting for client hello + std::vector frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) { + return handle_handshake_frame_error_(aerr); + } + // ignore contents, may be used in future for flags + // Reserve space for: existing prologue + 2 size bytes + frame data + prologue_.reserve(prologue_.size() + 2 + frame.size()); + prologue_.push_back((uint8_t) (frame.size() >> 8)); + prologue_.push_back((uint8_t) frame.size()); + prologue_.insert(prologue_.end(), frame.begin(), frame.end()); + + state_ = State::SERVER_HELLO; + } + if (state_ == State::SERVER_HELLO) { + // send server hello + const std::string &name = App.get_name(); + const std::string &mac = get_mac_address(); + + std::vector msg; + // Reserve space for: 1 byte proto + name + null + mac + null + msg.reserve(1 + name.size() + 1 + mac.size() + 1); + + // chosen proto + msg.push_back(0x01); + + // node name, terminated by null byte + const uint8_t *name_ptr = reinterpret_cast(name.c_str()); + msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1); + // node mac, terminated by null byte + const uint8_t *mac_ptr = reinterpret_cast(mac.c_str()); + msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1); + + aerr = write_frame_(msg.data(), msg.size()); + if (aerr != APIError::OK) + return aerr; + + // start handshake + aerr = init_handshake_(); + if (aerr != APIError::OK) + return aerr; + + state_ = State::HANDSHAKE; + } + if (state_ == State::HANDSHAKE) { + int action = noise_handshakestate_get_action(handshake_); + if (action == NOISE_ACTION_READ_MESSAGE) { + // waiting for handshake msg + std::vector frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) { + return handle_handshake_frame_error_(aerr); + } + + if (frame.empty()) { + send_explicit_handshake_reject_("Empty handshake message"); + return APIError::BAD_HANDSHAKE_ERROR_BYTE; + } else if (frame[0] != 0x00) { + HELPER_LOG("Bad handshake error byte: %u", frame[0]); + send_explicit_handshake_reject_("Bad handshake error byte"); + return APIError::BAD_HANDSHAKE_ERROR_BYTE; + } + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1); + err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); + if (err != 0) { + // Special handling for MAC failure + send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error"); + return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED); + } + + aerr = check_handshake_finished_(); + if (aerr != APIError::OK) + return aerr; + } else if (action == NOISE_ACTION_WRITE_MESSAGE) { + uint8_t buffer[65]; + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); + + err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); + APIError aerr_write = + handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED); + if (aerr_write != APIError::OK) + return aerr_write; + buffer[0] = 0x00; // success + + aerr = write_frame_(buffer, mbuf.size + 1); + if (aerr != APIError::OK) + return aerr; + aerr = check_handshake_finished_(); + if (aerr != APIError::OK) + return aerr; + } else { + // bad state for action + state_ = State::FAILED; + HELPER_LOG("Bad action for handshake: %d", action); + return APIError::HANDSHAKESTATE_BAD_STATE; + } + } + if (state_ == State::CLOSED || state_ == State::FAILED) { + return APIError::BAD_STATE; + } + return APIError::OK; +} +void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { + std::vector data; + data.resize(reason.length() + 1); + data[0] = 0x01; // failure + + // Copy error message in bulk + if (!reason.empty()) { + std::memcpy(data.data() + 1, reason.c_str(), reason.length()); + } + + // temporarily remove failed state + auto orig_state = state_; + state_ = State::EXPLICIT_REJECT; + write_frame_(data.data(), data.size()); + state_ = orig_state; +} +APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { + int err; + APIError aerr; + aerr = state_action_(); + if (aerr != APIError::OK) { + return aerr; + } + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + std::vector frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) + return aerr; + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); + err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); + APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED); + if (decrypt_err != APIError::OK) + return decrypt_err; + + uint16_t msg_size = mbuf.size; + uint8_t *msg_data = frame.data(); + if (msg_size < 4) { + state_ = State::FAILED; + HELPER_LOG("Bad data packet: size %d too short", msg_size); + return APIError::BAD_DATA_PACKET; + } + + uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; + uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; + if (data_len > msg_size - 4) { + state_ = State::FAILED; + HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); + return APIError::BAD_DATA_PACKET; + } + + buffer->container = std::move(frame); + buffer->data_offset = 4; + buffer->data_len = data_len; + buffer->type = type; + return APIError::OK; +} +APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { + // Resize to include MAC space (required for Noise encryption) + buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); + PacketInfo packet{type, 0, + static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; + return write_protobuf_packets(buffer, std::span(&packet, 1)); +} + +APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { + APIError aerr = state_action_(); + if (aerr != APIError::OK) { + return aerr; + } + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + if (packets.empty()) { + return APIError::OK; + } + + std::vector *raw_buffer = buffer.get_buffer(); + uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + + this->reusable_iovs_.clear(); + this->reusable_iovs_.reserve(packets.size()); + uint16_t total_write_len = 0; + + // We need to encrypt each packet in place + for (const auto &packet : packets) { + // The buffer already has padding at offset + uint8_t *buf_start = buffer_data + packet.offset; + + // Write noise header + buf_start[0] = 0x01; // indicator + // buf_start[1], buf_start[2] to be set after encryption + + // Write message header (to be encrypted) + const uint8_t msg_offset = 3; + buf_start[msg_offset] = static_cast(packet.message_type >> 8); // type high byte + buf_start[msg_offset + 1] = static_cast(packet.message_type); // type low byte + buf_start[msg_offset + 2] = static_cast(packet.payload_size >> 8); // data_len high byte + buf_start[msg_offset + 3] = static_cast(packet.payload_size); // data_len low byte + // payload data is already in the buffer starting at offset + 7 + + // Make sure we have space for MAC + // The buffer should already have been sized appropriately + + // Encrypt the message in place + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size, + 4 + packet.payload_size + frame_footer_size_); + + int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); + APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED); + if (aerr != APIError::OK) + return aerr; + + // Fill in the encrypted size + buf_start[1] = static_cast(mbuf.size >> 8); + buf_start[2] = static_cast(mbuf.size); + + // Add iovec for this encrypted packet + size_t packet_len = static_cast(3 + mbuf.size); // indicator + size + encrypted data + this->reusable_iovs_.push_back({buf_start, packet_len}); + total_write_len += packet_len; + } + + // Send all encrypted packets in one writev call + return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); +} + +APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { + uint8_t header[3]; + header[0] = 0x01; // indicator + header[1] = (uint8_t) (len >> 8); + header[2] = (uint8_t) len; + + struct iovec iov[2]; + iov[0].iov_base = header; + iov[0].iov_len = 3; + if (len == 0) { + return this->write_raw_(iov, 1, 3); // Just header + } + iov[1].iov_base = const_cast(data); + iov[1].iov_len = len; + + return this->write_raw_(iov, 2, 3 + len); // Header + data +} + +/** Initiate the data structures for the handshake. + * + * @return 0 on success, -1 on error (check errno) + */ +APIError APINoiseFrameHelper::init_handshake_() { + int err; + memset(&nid_, 0, sizeof(nid_)); + // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; + // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto)); + nid_.pattern_id = NOISE_PATTERN_NN; + nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY; + nid_.dh_id = NOISE_DH_CURVE25519; + nid_.prefix_id = NOISE_PREFIX_STANDARD; + nid_.hybrid_id = NOISE_DH_NONE; + nid_.hash_id = NOISE_HASH_SHA256; + nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; + + err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); + APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED); + if (aerr != APIError::OK) + return aerr; + + const auto &psk = ctx_->get_psk(); + err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); + aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED); + if (aerr != APIError::OK) + return aerr; + + err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); + aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED); + if (aerr != APIError::OK) + return aerr; + // set_prologue copies it into handshakestate, so we can get rid of it now + prologue_ = {}; + + err = noise_handshakestate_start(handshake_); + aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED); + if (aerr != APIError::OK) + return aerr; + return APIError::OK; +} + +APIError APINoiseFrameHelper::check_handshake_finished_() { + assert(state_ == State::HANDSHAKE); + + int action = noise_handshakestate_get_action(handshake_); + if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) + return APIError::OK; + if (action != NOISE_ACTION_SPLIT) { + state_ = State::FAILED; + HELPER_LOG("Bad action for handshake: %d", action); + return APIError::HANDSHAKESTATE_BAD_STATE; + } + int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); + APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED); + if (aerr != APIError::OK) + return aerr; + + frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_); + + HELPER_LOG("Handshake complete!"); + noise_handshakestate_free(handshake_); + handshake_ = nullptr; + state_ = State::DATA; + return APIError::OK; +} + +APINoiseFrameHelper::~APINoiseFrameHelper() { + if (handshake_ != nullptr) { + noise_handshakestate_free(handshake_); + handshake_ = nullptr; + } + if (send_cipher_ != nullptr) { + noise_cipherstate_free(send_cipher_); + send_cipher_ = nullptr; + } + if (recv_cipher_ != nullptr) { + noise_cipherstate_free(recv_cipher_); + recv_cipher_ = nullptr; + } +} + +extern "C" { +// declare how noise generates random bytes (here with a good HWRNG based on the RF system) +void noise_rand_bytes(void *output, size_t len) { + if (!esphome::random_bytes(reinterpret_cast(output), len)) { + ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting"); + arch_restart(); + } +} +} + +} // namespace api +} // namespace esphome +#endif // USE_API_NOISE +#endif // USE_API diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h new file mode 100644 index 0000000000..ed5141d625 --- /dev/null +++ b/esphome/components/api/api_frame_helper_noise.h @@ -0,0 +1,70 @@ +#pragma once +#include "api_frame_helper.h" +#ifdef USE_API +#ifdef USE_API_NOISE +#include "noise/protocol.h" +#include "api_noise_context.h" + +namespace esphome { +namespace api { + +class APINoiseFrameHelper : public APIFrameHelper { + public: + APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx, + const ClientInfo *client_info) + : APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) { + // Noise header structure: + // Pos 0: indicator (0x01) + // Pos 1-2: encrypted payload size (16-bit big-endian) + // Pos 3-6: encrypted type (16-bit) + data_len (16-bit) + // Pos 7+: actual payload data + frame_header_padding_ = 7; + } + ~APINoiseFrameHelper() override; + APIError init() override; + APIError loop() override; + APIError read_packet(ReadPacketBuffer *buffer) override; + APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; + APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; + // Get the frame header padding required by this protocol + uint8_t frame_header_padding() override { return frame_header_padding_; } + // Get the frame footer size required by this protocol + uint8_t frame_footer_size() override { return frame_footer_size_; } + + protected: + APIError state_action_(); + APIError try_read_frame_(std::vector *frame); + APIError write_frame_(const uint8_t *data, uint16_t len); + APIError init_handshake_(); + APIError check_handshake_finished_(); + void send_explicit_handshake_reject_(const std::string &reason); + APIError handle_handshake_frame_error_(APIError aerr); + APIError handle_noise_error_(int err, const char *func_name, APIError api_err); + + // Pointers first (4 bytes each) + NoiseHandshakeState *handshake_{nullptr}; + NoiseCipherState *send_cipher_{nullptr}; + NoiseCipherState *recv_cipher_{nullptr}; + + // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) + std::shared_ptr ctx_; + + // Vector (12 bytes on 32-bit) + std::vector prologue_; + + // NoiseProtocolId (size depends on implementation) + NoiseProtocolId nid_; + + // Group small types together + // Fixed-size header buffer for noise protocol: + // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) + // Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase + uint8_t rx_header_buf_[3]; + uint8_t rx_header_buf_len_ = 0; + // 4 bytes total, no padding +}; + +} // namespace api +} // namespace esphome +#endif // USE_API_NOISE +#endif // USE_API diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp new file mode 100644 index 0000000000..d0bc631e1b --- /dev/null +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -0,0 +1,292 @@ +#include "api_frame_helper_plaintext.h" +#ifdef USE_API +#ifdef USE_API_PLAINTEXT +#include "api_connection.h" // For ClientInfo struct +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "proto.h" +#include +#include + +namespace esphome { +namespace api { + +static const char *const TAG = "api.plaintext"; + +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) + +#ifdef HELPER_LOG_PACKETS +#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) +#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) +#else +#define LOG_PACKET_RECEIVED(buffer) ((void) 0) +#define LOG_PACKET_SENDING(data, len) ((void) 0) +#endif + +/// Initialize the frame helper, returns OK if successful. +APIError APIPlaintextFrameHelper::init() { + APIError err = init_common_(); + if (err != APIError::OK) { + return err; + } + + state_ = State::DATA; + return APIError::OK; +} +APIError APIPlaintextFrameHelper::loop() { + if (state_ != State::DATA) { + return APIError::BAD_STATE; + } + // Use base class implementation for buffer sending + return APIFrameHelper::loop(); +} + +/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter + * + * @param frame: The struct to hold the frame information in. + * msg: store the parsed frame in that struct + * + * @return See APIError + * + * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. + */ +APIError APIPlaintextFrameHelper::try_read_frame_(std::vector *frame) { + if (frame == nullptr) { + HELPER_LOG("Bad argument for try_read_frame_"); + return APIError::BAD_ARG; + } + + // read header + while (!rx_header_parsed_) { + // Now that we know when the socket is ready, we can read up to 3 bytes + // into the rx_header_buf_ before we have to switch back to reading + // one byte at a time to ensure we don't read past the message and + // into the next one. + + // Read directly into rx_header_buf_ at the current position + // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time + ssize_t received = + this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1); + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; + } + + // If this was the first read, validate the indicator byte + if (rx_header_buf_pos_ == 0 && received > 0) { + if (rx_header_buf_[0] != 0x00) { + state_ = State::FAILED; + HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); + return APIError::BAD_INDICATOR; + } + } + + rx_header_buf_pos_ += received; + + // Check for buffer overflow + if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) { + state_ = State::FAILED; + HELPER_LOG("Header buffer overflow"); + return APIError::BAD_DATA_PACKET; + } + + // Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse + if (rx_header_buf_pos_ < 3) { + continue; + } + + // At this point, we have at least 3 bytes total: + // - Validated indicator byte (0x00) stored at position 0 + // - At least 2 bytes in the buffer for the varints + // Buffer layout: + // [0]: indicator byte (0x00) + // [1-3]: Message size varint (variable length) + // - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535) + // - 3 bytes allows up to 2097151, ensuring we support at least as much as noise + // [2-5]: Message type varint (variable length) + // We now attempt to parse both varints. If either is incomplete, + // we'll continue reading more bytes. + + // Skip indicator byte at position 0 + uint8_t varint_pos = 1; + uint32_t consumed = 0; + + auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); + if (!msg_size_varint.has_value()) { + // not enough data there yet + continue; + } + + if (msg_size_varint->as_uint32() > std::numeric_limits::max()) { + state_ = State::FAILED; + HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(), + std::numeric_limits::max()); + return APIError::BAD_DATA_PACKET; + } + rx_header_parsed_len_ = msg_size_varint->as_uint16(); + + // Move to next varint position + varint_pos += consumed; + + auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); + if (!msg_type_varint.has_value()) { + // not enough data there yet + continue; + } + if (msg_type_varint->as_uint32() > std::numeric_limits::max()) { + state_ = State::FAILED; + HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(), + std::numeric_limits::max()); + return APIError::BAD_DATA_PACKET; + } + rx_header_parsed_type_ = msg_type_varint->as_uint16(); + rx_header_parsed_ = true; + } + // header reading done + + // reserve space for body + if (rx_buf_.size() != rx_header_parsed_len_) { + rx_buf_.resize(rx_header_parsed_len_); + } + + if (rx_buf_len_ < rx_header_parsed_len_) { + // more data to read + uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_; + ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read); + APIError err = handle_socket_read_result_(received); + if (err != APIError::OK) { + return err; + } + rx_buf_len_ += static_cast(received); + if (static_cast(received) != to_read) { + // not all read + return APIError::WOULD_BLOCK; + } + } + + LOG_PACKET_RECEIVED(rx_buf_); + *frame = std::move(rx_buf_); + // consume msg + rx_buf_ = {}; + rx_buf_len_ = 0; + rx_header_buf_pos_ = 0; + rx_header_parsed_ = false; + return APIError::OK; +} +APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { + APIError aerr; + + if (state_ != State::DATA) { + return APIError::WOULD_BLOCK; + } + + std::vector frame; + aerr = try_read_frame_(&frame); + if (aerr != APIError::OK) { + if (aerr == APIError::BAD_INDICATOR) { + // Make sure to tell the remote that we don't + // understand the indicator byte so it knows + // we do not support it. + struct iovec iov[1]; + // The \x00 first byte is the marker for plaintext. + // + // The remote will know how to handle the indicator byte, + // but it likely won't understand the rest of the message. + // + // We must send at least 3 bytes to be read, so we add + // a message after the indicator byte to ensures its long + // enough and can aid in debugging. + const char msg[] = "\x00" + "Bad indicator byte"; + iov[0].iov_base = (void *) msg; + iov[0].iov_len = 19; + this->write_raw_(iov, 1, 19); + } + return aerr; + } + + buffer->container = std::move(frame); + buffer->data_offset = 0; + buffer->data_len = rx_header_parsed_len_; + buffer->type = rx_header_parsed_type_; + return APIError::OK; +} +APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { + PacketInfo packet{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_)}; + return write_protobuf_packets(buffer, std::span(&packet, 1)); +} + +APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { + if (state_ != State::DATA) { + return APIError::BAD_STATE; + } + + if (packets.empty()) { + return APIError::OK; + } + + std::vector *raw_buffer = buffer.get_buffer(); + uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + + this->reusable_iovs_.clear(); + this->reusable_iovs_.reserve(packets.size()); + uint16_t total_write_len = 0; + + for (const auto &packet : packets) { + // Calculate varint sizes for header layout + uint8_t size_varint_len = api::ProtoSize::varint(static_cast(packet.payload_size)); + uint8_t type_varint_len = api::ProtoSize::varint(static_cast(packet.message_type)); + uint8_t total_header_len = 1 + size_varint_len + type_varint_len; + + // Calculate where to start writing the header + // The header starts at the latest possible position to minimize unused padding + // + // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3 + // [0-2] - Unused padding + // [3] - 0x00 indicator byte + // [4] - Payload size varint (1 byte, for sizes 0-127) + // [5] - Message type varint (1 byte, for types 0-127) + // [6...] - Actual payload data + // + // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2 + // [0-1] - Unused padding + // [2] - 0x00 indicator byte + // [3-4] - Payload size varint (2 bytes, for sizes 128-16383) + // [5] - Message type varint (1 byte, for types 0-127) + // [6...] - Actual payload data + // + // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0 + // [0] - 0x00 indicator byte + // [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151) + // [4-5] - Message type varint (2 bytes, for types 128-32767) + // [6...] - Actual payload data + // + // The message starts at offset + frame_header_padding_ + // So we write the header starting at offset + frame_header_padding_ - total_header_len + uint8_t *buf_start = buffer_data + packet.offset; + uint32_t header_offset = frame_header_padding_ - total_header_len; + + // Write the plaintext header + buf_start[header_offset] = 0x00; // indicator + + // Encode varints directly into buffer + ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); + ProtoVarInt(packet.message_type) + .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); + + // Add iovec for this packet (header + payload) + size_t packet_len = static_cast(total_header_len + packet.payload_size); + this->reusable_iovs_.push_back({buf_start + header_offset, packet_len}); + total_write_len += packet_len; + } + + // Send all packets in one writev call + return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); +} + +} // namespace api +} // namespace esphome +#endif // USE_API_PLAINTEXT +#endif // USE_API diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h new file mode 100644 index 0000000000..465ceae827 --- /dev/null +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -0,0 +1,55 @@ +#pragma once +#include "api_frame_helper.h" +#ifdef USE_API +#ifdef USE_API_PLAINTEXT + +namespace esphome { +namespace api { + +class APIPlaintextFrameHelper : public APIFrameHelper { + public: + APIPlaintextFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) + : APIFrameHelper(std::move(socket), client_info) { + // Plaintext header structure (worst case): + // Pos 0: indicator (0x00) + // Pos 1-3: payload size varint (up to 3 bytes) + // Pos 4-5: message type varint (up to 2 bytes) + // Pos 6+: actual payload data + frame_header_padding_ = 6; + } + ~APIPlaintextFrameHelper() override = default; + APIError init() override; + APIError loop() override; + APIError read_packet(ReadPacketBuffer *buffer) override; + APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; + APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; + uint8_t frame_header_padding() override { return frame_header_padding_; } + // Get the frame footer size required by this protocol + uint8_t frame_footer_size() override { return frame_footer_size_; } + + protected: + APIError try_read_frame_(std::vector *frame); + + // Group 2-byte aligned types + uint16_t rx_header_parsed_type_ = 0; + uint16_t rx_header_parsed_len_ = 0; + + // Group 1-byte types together + // Fixed-size header buffer for plaintext protocol: + // We now store the indicator byte + the two varints. + // To match noise protocol's maximum message size (UINT16_MAX = 65535), we need: + // 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint + // + // While varints could theoretically be up to 10 bytes each for 64-bit values, + // attempting to process messages with headers that large would likely crash the + // ESP32 due to memory constraints. + uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type) + uint8_t rx_header_buf_pos_ = 0; + bool rx_header_parsed_ = false; + // 8 bytes total, no padding needed +}; + +} // namespace api +} // namespace esphome +#endif // USE_API_PLAINTEXT +#endif // USE_API 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 3505ec758d..528c581ad7 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1,74 +1,35 @@ // This file was automatically generated with a tool. // See script/api_protobuf/api_protobuf.py #include "api_pb2.h" -#include "api_pb2_size.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; - } - default: - return false; - } -} -void HelloRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->client_info); - buffer.encode_uint32(2, this->api_version_major); - buffer.encode_uint32(3, this->api_version_minor); -} -void HelloRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->client_info, false); - ProtoSize::add_uint32_field(total_size, 1, this->api_version_major, false); - ProtoSize::add_uint32_field(total_size, 1, this->api_version_minor, false); -} -bool HelloResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->api_version_major = value.as_uint32(); - return true; - } - case 2: { - this->api_version_minor = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool HelloResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 3: { - this->server_info = value.as_string(); - return true; - } - case 4: { - this->name = 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); @@ -77,1083 +38,465 @@ void HelloResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(4, this->name); } void HelloResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint32_field(total_size, 1, this->api_version_major, false); - ProtoSize::add_uint32_field(total_size, 1, this->api_version_minor, false); - ProtoSize::add_string_field(total_size, 1, this->server_info, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); + ProtoSize::add_uint32_field(total_size, 1, this->api_version_major); + ProtoSize::add_uint32_field(total_size, 1, this->api_version_minor); + ProtoSize::add_string_field(total_size, 1, this->server_info); + ProtoSize::add_string_field(total_size, 1, this->name); } bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->password = value.as_string(); - return true; - } - default: - return false; - } -} -void ConnectRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->password); } -void ConnectRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->password, false); -} -bool ConnectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->invalid_password = value.as_bool(); - 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, false); -} -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; - } + ProtoSize::add_bool_field(total_size, 1, this->invalid_password); } +#ifdef USE_AREAS void AreaInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->area_id); buffer.encode_string(2, this->name); } void AreaInfo::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint32_field(total_size, 1, this->area_id, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); -} -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; - } + ProtoSize::add_uint32_field(total_size, 1, this->area_id); + ProtoSize::add_string_field(total_size, 1, this->name); } +#endif +#ifdef USE_DEVICES void DeviceInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->device_id); buffer.encode_string(2, this->name); buffer.encode_uint32(3, this->area_id); } void DeviceInfo::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_uint32_field(total_size, 1, this->area_id, false); -} -bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->uses_password = value.as_bool(); - return true; - } - case 7: { - this->has_deep_sleep = value.as_bool(); - return true; - } - case 10: { - this->webserver_port = value.as_uint32(); - return true; - } - case 11: { - this->legacy_bluetooth_proxy_version = value.as_uint32(); - return true; - } - case 15: { - this->bluetooth_proxy_feature_flags = value.as_uint32(); - return true; - } - case 14: { - this->legacy_voice_assistant_version = value.as_uint32(); - return true; - } - case 17: { - this->voice_assistant_feature_flags = value.as_uint32(); - return true; - } - case 19: { - this->api_encryption_supported = value.as_bool(); - return true; - } - default: - return false; - } -} -bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->name = value.as_string(); - return true; - } - case 3: { - this->mac_address = value.as_string(); - return true; - } - case 4: { - this->esphome_version = value.as_string(); - return true; - } - case 5: { - this->compilation_time = value.as_string(); - return true; - } - case 6: { - this->model = value.as_string(); - return true; - } - case 8: { - this->project_name = value.as_string(); - return true; - } - case 9: { - this->project_version = value.as_string(); - return true; - } - case 12: { - this->manufacturer = value.as_string(); - return true; - } - case 13: { - this->friendly_name = value.as_string(); - return true; - } - case 16: { - this->suggested_area = value.as_string(); - return true; - } - case 18: { - this->bluetooth_mac_address = value.as_string(); - return true; - } - case 20: { - this->devices.push_back(value.as_message()); - return true; - } - case 21: { - this->areas.push_back(value.as_message()); - return true; - } - case 22: { - this->area = value.as_message(); - return true; - } - default: - return false; - } + ProtoSize::add_uint32_field(total_size, 1, this->device_id); + ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_uint32_field(total_size, 1, this->area_id); } +#endif void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { +#ifdef USE_API_PASSWORD buffer.encode_bool(1, this->uses_password); +#endif buffer.encode_string(2, this->name); buffer.encode_string(3, this->mac_address); 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); - 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); - buffer.encode_uint32(14, this->legacy_voice_assistant_version); +#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); + buffer.encode_message(20, it, true); } +#endif +#ifdef USE_AREAS for (auto &it : this->areas) { - buffer.encode_message(21, it, true); + buffer.encode_message(21, it, true); } - buffer.encode_message(22, this->area); +#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, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->mac_address, false); - ProtoSize::add_string_field(total_size, 1, this->esphome_version, false); - ProtoSize::add_string_field(total_size, 1, this->compilation_time, false); - ProtoSize::add_string_field(total_size, 1, this->model, false); - ProtoSize::add_bool_field(total_size, 1, this->has_deep_sleep, false); - ProtoSize::add_string_field(total_size, 1, this->project_name, false); - ProtoSize::add_string_field(total_size, 1, this->project_version, false); - ProtoSize::add_uint32_field(total_size, 1, this->webserver_port, false); - ProtoSize::add_uint32_field(total_size, 1, this->legacy_bluetooth_proxy_version, false); - ProtoSize::add_uint32_field(total_size, 1, this->bluetooth_proxy_feature_flags, false); - ProtoSize::add_string_field(total_size, 1, this->manufacturer, false); - ProtoSize::add_string_field(total_size, 1, this->friendly_name, false); - ProtoSize::add_uint32_field(total_size, 1, this->legacy_voice_assistant_version, false); - ProtoSize::add_uint32_field(total_size, 2, this->voice_assistant_feature_flags, false); - ProtoSize::add_string_field(total_size, 2, this->suggested_area, false); - ProtoSize::add_string_field(total_size, 2, this->bluetooth_mac_address, false); - ProtoSize::add_bool_field(total_size, 2, this->api_encryption_supported, false); +#ifdef USE_API_PASSWORD + ProtoSize::add_bool_field(total_size, 1, this->uses_password); +#endif + ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_string_field(total_size, 1, this->mac_address); + 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->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, 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); - ProtoSize::add_message_object(total_size, 2, this->area, false); +#endif +#ifdef USE_AREAS + ProtoSize::add_message_object(total_size, 2, this->area); +#endif } #ifdef USE_BINARY_SENSOR -bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->is_status_binary_sensor = value.as_bool(); - return true; - } - case 7: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 9: { - this->entity_category = value.as_enum(); - return true; - } - case 10: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesBinarySensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->device_class = value.as_string(); - return true; - } - case 8: { - this->icon = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesBinarySensorResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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); - buffer.encode_enum(9, this->entity_category); +#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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->device_class, false); - ProtoSize::add_bool_field(total_size, 1, this->is_status_binary_sensor, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool BinarySensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->state = value.as_bool(); - return true; - } - case 3: { - this->missing_state = value.as_bool(); - return true; - } - case 4: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool BinarySensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + 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->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, false); - ProtoSize::add_bool_field(total_size, 1, this->state, false); - ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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 -bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 5: { - this->assumed_state = value.as_bool(); - return true; - } - case 6: { - this->supports_position = value.as_bool(); - return true; - } - case 7: { - this->supports_tilt = value.as_bool(); - return true; - } - case 9: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 11: { - this->entity_category = value.as_enum(); - return true; - } - case 12: { - this->supports_stop = value.as_bool(); - return true; - } - case 13: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesCoverResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 8: { - this->device_class = value.as_string(); - return true; - } - case 10: { - this->icon = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesCoverResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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); - buffer.encode_enum(11, this->entity_category); +#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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false); - ProtoSize::add_bool_field(total_size, 1, this->supports_position, false); - ProtoSize::add_bool_field(total_size, 1, this->supports_tilt, false); - ProtoSize::add_string_field(total_size, 1, this->device_class, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->legacy_state = value.as_enum(); - return true; - } - case 5: { - this->current_operation = value.as_enum(); - return true; - } - case 6: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool CoverStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - case 3: { - this->position = value.as_float(); - return true; - } - case 4: { - this->tilt = value.as_float(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); + 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); - buffer.encode_enum(2, this->legacy_state); buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); - buffer.encode_enum(5, this->current_operation); + 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, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_state), false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + 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: { - this->has_legacy_command = value.as_bool(); - return true; - } - case 3: { - this->legacy_command = value.as_enum(); - return true; - } - case 4: { + 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; - } + break; +#ifdef USE_DEVICES + case 9: + this->device_id = value.as_uint32(); + 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; } -} -void CoverCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_bool(2, this->has_legacy_command); - buffer.encode_enum(3, this->legacy_command); - buffer.encode_bool(4, this->has_position); - buffer.encode_float(5, this->position); - buffer.encode_bool(6, this->has_tilt); - buffer.encode_float(7, this->tilt); - buffer.encode_bool(8, this->stop); -} -void CoverCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_bool_field(total_size, 1, this->has_legacy_command, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_command), false); - ProtoSize::add_bool_field(total_size, 1, this->has_position, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->has_tilt, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->stop, false); + return true; } #endif #ifdef USE_FAN -bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 5: { - this->supports_oscillation = value.as_bool(); - return true; - } - case 6: { - this->supports_speed = value.as_bool(); - return true; - } - case 7: { - this->supports_direction = value.as_bool(); - return true; - } - case 8: { - this->supported_speed_count = value.as_int32(); - return true; - } - case 9: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 11: { - this->entity_category = value.as_enum(); - return true; - } - case 13: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 10: { - this->icon = value.as_string(); - return true; - } - case 12: { - this->supported_preset_modes.push_back(value.as_string()); - return true; - } - default: - return false; - } -} -bool ListEntitiesFanResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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); - buffer.encode_enum(11, this->entity_category); +#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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_bool_field(total_size, 1, this->supports_oscillation, false); - ProtoSize::add_bool_field(total_size, 1, this->supports_speed, false); - ProtoSize::add_bool_field(total_size, 1, this->supports_direction, false); - ProtoSize::add_int32_field(total_size, 1, this->supported_speed_count, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); + 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(total_size, 1, it, true); + ProtoSize::add_string_field_repeated(total_size, 1, it); } } - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->state = value.as_bool(); - return true; - } - case 3: { - this->oscillating = value.as_bool(); - return true; - } - case 4: { - this->speed = value.as_enum(); - return true; - } - case 5: { - this->direction = value.as_enum(); - return true; - } - case 6: { - this->speed_level = value.as_int32(); - return true; - } - case 8: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool FanStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 7: { - this->preset_mode = value.as_string(); - return true; - } - default: - return false; - } -} -bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } +#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); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); - buffer.encode_enum(4, this->speed); - buffer.encode_enum(5, this->direction); + 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, false); - ProtoSize::add_bool_field(total_size, 1, this->state, false); - ProtoSize::add_bool_field(total_size, 1, this->oscillating, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->speed), false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->direction), false); - ProtoSize::add_int32_field(total_size, 1, this->speed_level, false); - ProtoSize::add_string_field(total_size, 1, this->preset_mode, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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->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: { - this->has_speed = value.as_bool(); - return true; - } - case 5: { - this->speed = value.as_enum(); - 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: { - this->direction = value.as_enum(); - return true; - } - case 10: { + break; + case 9: + this->direction = static_cast(value.as_uint32()); + 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; - } + break; +#ifdef USE_DEVICES + case 14: + this->device_id = value.as_uint32(); + 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; } -} -void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_bool(2, this->has_state); - buffer.encode_bool(3, this->state); - buffer.encode_bool(4, this->has_speed); - buffer.encode_enum(5, this->speed); - buffer.encode_bool(6, this->has_oscillating); - buffer.encode_bool(7, this->oscillating); - buffer.encode_bool(8, this->has_direction); - buffer.encode_enum(9, this->direction); - buffer.encode_bool(10, this->has_speed_level); - buffer.encode_int32(11, this->speed_level); - buffer.encode_bool(12, this->has_preset_mode); - buffer.encode_string(13, this->preset_mode); -} -void FanCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_bool_field(total_size, 1, this->has_state, false); - ProtoSize::add_bool_field(total_size, 1, this->state, false); - ProtoSize::add_bool_field(total_size, 1, this->has_speed, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->speed), false); - ProtoSize::add_bool_field(total_size, 1, this->has_oscillating, false); - ProtoSize::add_bool_field(total_size, 1, this->oscillating, false); - ProtoSize::add_bool_field(total_size, 1, this->has_direction, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->direction), false); - ProtoSize::add_bool_field(total_size, 1, this->has_speed_level, false); - ProtoSize::add_int32_field(total_size, 1, this->speed_level, false); - ProtoSize::add_bool_field(total_size, 1, this->has_preset_mode, false); - ProtoSize::add_string_field(total_size, 1, this->preset_mode, false); + return true; } #endif #ifdef USE_LIGHT -bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 12: { - this->supported_color_modes.push_back(value.as_enum()); - return true; - } - case 5: { - this->legacy_supports_brightness = value.as_bool(); - return true; - } - case 6: { - this->legacy_supports_rgb = value.as_bool(); - return true; - } - case 7: { - this->legacy_supports_white_value = value.as_bool(); - return true; - } - case 8: { - this->legacy_supports_color_temperature = value.as_bool(); - return true; - } - case 13: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 15: { - this->entity_category = value.as_enum(); - return true; - } - case 16: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesLightResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 11: { - this->effects.push_back(value.as_string()); - return true; - } - case 14: { - this->icon = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesLightResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - case 9: { - this->min_mireds = value.as_float(); - return true; - } - case 10: { - this->max_mireds = value.as_float(); - return true; - } - default: - return false; - } -} 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_enum(12, it, true); + buffer.encode_uint32(12, static_cast(it), true); } - buffer.encode_bool(5, this->legacy_supports_brightness); - buffer.encode_bool(6, this->legacy_supports_rgb); - buffer.encode_bool(7, this->legacy_supports_white_value); - buffer.encode_bool(8, this->legacy_supports_color_temperature); buffer.encode_float(9, this->min_mireds); buffer.encode_float(10, this->max_mireds); for (auto &it : this->effects) { buffer.encode_string(11, it, true); } buffer.encode_bool(13, this->disabled_by_default); +#ifdef USE_ENTITY_ICON buffer.encode_string(14, this->icon); - buffer.encode_enum(15, this->entity_category); +#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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); if (!this->supported_color_modes.empty()) { for (const auto &it : this->supported_color_modes) { - ProtoSize::add_enum_field(total_size, 1, static_cast(it), true); + ProtoSize::add_enum_field_repeated(total_size, 1, static_cast(it)); } } - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_brightness, false); - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_rgb, false); - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_white_value, false); - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_color_temperature, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->min_mireds != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->max_mireds != 0.0f, false); + 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(total_size, 1, it, true); + ProtoSize::add_string_field_repeated(total_size, 1, it); } } - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_uint32_field(total_size, 2, this->device_id, false); -} -bool LightStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->state = value.as_bool(); - return true; - } - case 11: { - this->color_mode = value.as_enum(); - return true; - } - case 14: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool LightStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 9: { - this->effect = value.as_string(); - return true; - } - default: - return false; - } -} -bool LightStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - case 3: { - this->brightness = value.as_float(); - return true; - } - case 10: { - this->color_brightness = value.as_float(); - return true; - } - case 4: { - this->red = value.as_float(); - return true; - } - case 5: { - this->green = value.as_float(); - return true; - } - case 6: { - this->blue = value.as_float(); - return true; - } - case 7: { - this->white = value.as_float(); - return true; - } - case 8: { - this->color_temperature = value.as_float(); - return true; - } - case 12: { - this->cold_white = value.as_float(); - return true; - } - case 13: { - this->warm_white = value.as_float(); - return true; - } - default: - return false; - } + 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); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); - buffer.encode_enum(11, this->color_mode); + buffer.encode_uint32(11, static_cast(this->color_mode)); buffer.encode_float(10, this->color_brightness); buffer.encode_float(4, this->red); buffer.encode_float(5, this->green); @@ -1163,812 +506,373 @@ 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, false); - ProtoSize::add_bool_field(total_size, 1, this->state, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->brightness != 0.0f, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->color_mode), false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->color_brightness != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->red != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->green != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->blue != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->white != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->color_temperature != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->cold_white != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->warm_white != 0.0f, false); - ProtoSize::add_string_field(total_size, 1, this->effect, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_bool_field(total_size, 1, this->state); + ProtoSize::add_float_field(total_size, 1, this->brightness); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->color_mode)); + 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: { - this->color_mode = value.as_enum(); - return true; - } - case 20: { + break; + case 23: + this->color_mode = static_cast(value.as_uint32()); + 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; - } + break; +#ifdef USE_DEVICES + case 28: + this->device_id = value.as_uint32(); + 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; } -} -void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_bool(2, this->has_state); - buffer.encode_bool(3, this->state); - buffer.encode_bool(4, this->has_brightness); - buffer.encode_float(5, this->brightness); - buffer.encode_bool(22, this->has_color_mode); - buffer.encode_enum(23, this->color_mode); - buffer.encode_bool(20, this->has_color_brightness); - buffer.encode_float(21, this->color_brightness); - buffer.encode_bool(6, this->has_rgb); - buffer.encode_float(7, this->red); - buffer.encode_float(8, this->green); - buffer.encode_float(9, this->blue); - buffer.encode_bool(10, this->has_white); - buffer.encode_float(11, this->white); - buffer.encode_bool(12, this->has_color_temperature); - buffer.encode_float(13, this->color_temperature); - buffer.encode_bool(24, this->has_cold_white); - buffer.encode_float(25, this->cold_white); - buffer.encode_bool(26, this->has_warm_white); - buffer.encode_float(27, this->warm_white); - buffer.encode_bool(14, this->has_transition_length); - buffer.encode_uint32(15, this->transition_length); - buffer.encode_bool(16, this->has_flash_length); - buffer.encode_uint32(17, this->flash_length); - buffer.encode_bool(18, this->has_effect); - buffer.encode_string(19, this->effect); -} -void LightCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_bool_field(total_size, 1, this->has_state, false); - ProtoSize::add_bool_field(total_size, 1, this->state, false); - ProtoSize::add_bool_field(total_size, 1, this->has_brightness, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->brightness != 0.0f, false); - ProtoSize::add_bool_field(total_size, 2, this->has_color_mode, false); - ProtoSize::add_enum_field(total_size, 2, static_cast(this->color_mode), false); - ProtoSize::add_bool_field(total_size, 2, this->has_color_brightness, false); - ProtoSize::add_fixed_field<4>(total_size, 2, this->color_brightness != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->has_rgb, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->red != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->green != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->blue != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->has_white, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->white != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->has_color_temperature, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->color_temperature != 0.0f, false); - ProtoSize::add_bool_field(total_size, 2, this->has_cold_white, false); - ProtoSize::add_fixed_field<4>(total_size, 2, this->cold_white != 0.0f, false); - ProtoSize::add_bool_field(total_size, 2, this->has_warm_white, false); - ProtoSize::add_fixed_field<4>(total_size, 2, this->warm_white != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->has_transition_length, false); - ProtoSize::add_uint32_field(total_size, 1, this->transition_length, false); - ProtoSize::add_bool_field(total_size, 2, this->has_flash_length, false); - ProtoSize::add_uint32_field(total_size, 2, this->flash_length, false); - ProtoSize::add_bool_field(total_size, 2, this->has_effect, false); - ProtoSize::add_string_field(total_size, 2, this->effect, false); + return true; } #endif #ifdef USE_SENSOR -bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 7: { - this->accuracy_decimals = value.as_int32(); - return true; - } - case 8: { - this->force_update = value.as_bool(); - return true; - } - case 10: { - this->state_class = value.as_enum(); - return true; - } - case 11: { - this->legacy_last_reset_type = value.as_enum(); - return true; - } - case 12: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 13: { - this->entity_category = value.as_enum(); - return true; - } - case 14: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesSensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 6: { - this->unit_of_measurement = value.as_string(); - return true; - } - case 9: { - this->device_class = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesSensorResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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); buffer.encode_string(9, this->device_class); - buffer.encode_enum(10, this->state_class); - buffer.encode_enum(11, this->legacy_last_reset_type); + buffer.encode_uint32(10, static_cast(this->state_class)); buffer.encode_bool(12, this->disabled_by_default); - buffer.encode_enum(13, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement, false); - ProtoSize::add_int32_field(total_size, 1, this->accuracy_decimals, false); - ProtoSize::add_bool_field(total_size, 1, this->force_update, false); - ProtoSize::add_string_field(total_size, 1, this->device_class, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->state_class), false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_last_reset_type), false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool SensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 3: { - this->missing_state = value.as_bool(); - return true; - } - case 4: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool SensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - case 2: { - this->state = value.as_float(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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); + ProtoSize::add_string_field(total_size, 1, this->device_class); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->state_class)); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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 -bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->assumed_state = value.as_bool(); - return true; - } - case 7: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 8: { - this->entity_category = value.as_enum(); - return true; - } - case 10: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesSwitchResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 9: { - this->device_class = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesSwitchResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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_enum(8, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_string_field(total_size, 1, this->device_class, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool SwitchStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->state = value.as_bool(); - return true; - } - case 3: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool SwitchStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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, false); - ProtoSize::add_bool_field(total_size, 1, this->state, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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; - } + break; +#ifdef USE_DEVICES + case 3: + this->device_id = value.as_uint32(); + 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; } -} -void SwitchCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_bool(2, this->state); -} -void SwitchCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_bool_field(total_size, 1, this->state, false); + return true; } #endif #ifdef USE_TEXT_SENSOR -bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 9: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesTextSensorResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 8: { - this->device_class = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesTextSensorResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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_enum(7, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_string_field(total_size, 1, this->device_class, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool TextSensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 3: { - this->missing_state = value.as_bool(); - return true; - } - case 4: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool TextSensorStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->state = value.as_string(); - return true; - } - default: - return false; - } -} -bool TextSensorStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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, false); - ProtoSize::add_string_field(total_size, 1, this->state, false); - ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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: { - this->level = value.as_enum(); - return true; - } - case 2: { + case 1: + this->level = static_cast(value.as_uint32()); + break; + case 2: this->dump_config = value.as_bool(); - return true; - } - default: - return false; - } -} -void SubscribeLogsRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_enum(1, this->level); - buffer.encode_bool(2, this->dump_config); -} -void SubscribeLogsRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_enum_field(total_size, 1, static_cast(this->level), false); - ProtoSize::add_bool_field(total_size, 1, this->dump_config, false); -} -bool SubscribeLogsResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->level = value.as_enum(); - return true; - } - case 4: { - this->send_failed = value.as_bool(); - return true; - } - default: - return false; - } -} -bool SubscribeLogsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 3: { - this->message = value.as_string(); - return true; - } + break; default: return false; } + return true; } void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_enum(1, this->level); + buffer.encode_uint32(1, static_cast(this->level)); buffer.encode_bytes(3, reinterpret_cast(this->message.data()), this->message.size()); buffer.encode_bool(4, this->send_failed); } void SubscribeLogsResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_enum_field(total_size, 1, static_cast(this->level), false); - ProtoSize::add_string_field(total_size, 1, this->message, false); - ProtoSize::add_bool_field(total_size, 1, this->send_failed, false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->level)); + ProtoSize::add_string_field(total_size, 1, this->message); + ProtoSize::add_bool_field(total_size, 1, this->send_failed); } #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; - } - default: - return false; - } -} -void NoiseEncryptionSetKeyRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_bytes(1, reinterpret_cast(this->key.data()), this->key.size()); -} -void NoiseEncryptionSetKeyRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->key, false); -} -bool NoiseEncryptionSetKeyResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->success = value.as_bool(); - 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, false); + 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); } void HomeassistantServiceMap::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->key, false); - ProtoSize::add_string_field(total_size, 1, this->value, false); -} -bool HomeassistantServiceResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 5: { - this->is_event = value.as_bool(); - return true; - } - default: - return false; - } -} -bool HomeassistantServiceResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->service = value.as_string(); - return true; - } - case 2: { - this->data.push_back(value.as_message()); - return true; - } - case 3: { - this->data_template.push_back(value.as_message()); - return true; - } - case 4: { - this->variables.push_back(value.as_message()); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->value); } void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->service); for (auto &it : this->data) { - buffer.encode_message(2, it, true); + buffer.encode_message(2, it, true); } for (auto &it : this->data_template) { - buffer.encode_message(3, it, true); + buffer.encode_message(3, it, true); } for (auto &it : this->variables) { - buffer.encode_message(4, it, true); + buffer.encode_message(4, it, true); } buffer.encode_bool(5, this->is_event); } void HomeassistantServiceResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->service, false); + ProtoSize::add_string_field(total_size, 1, this->service); ProtoSize::add_repeated_message(total_size, 1, this->data); ProtoSize::add_repeated_message(total_size, 1, this->data_template); ProtoSize::add_repeated_message(total_size, 1, this->variables); - ProtoSize::add_bool_field(total_size, 1, this->is_event, false); -} -bool SubscribeHomeAssistantStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 3: { - this->once = value.as_bool(); - return true; - } - default: - return false; - } -} -bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->entity_id = value.as_string(); - return true; - } - case 2: { - this->attribute = value.as_string(); - return true; - } - default: - return false; - } + ProtoSize::add_bool_field(total_size, 1, this->is_event); } void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->entity_id); @@ -1976,2108 +880,972 @@ void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const buffer.encode_bool(3, this->once); } void SubscribeHomeAssistantStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->entity_id, false); - ProtoSize::add_string_field(total_size, 1, this->attribute, false); - ProtoSize::add_bool_field(total_size, 1, this->once, false); + ProtoSize::add_string_field(total_size, 1, this->entity_id); + ProtoSize::add_string_field(total_size, 1, this->attribute); + ProtoSize::add_bool_field(total_size, 1, this->once); } 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; } -} -void HomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->entity_id); - buffer.encode_string(2, this->state); - buffer.encode_string(3, this->attribute); -} -void HomeAssistantStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->entity_id, false); - ProtoSize::add_string_field(total_size, 1, this->state, false); - ProtoSize::add_string_field(total_size, 1, this->attribute, 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, false); -} -bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->type = value.as_enum(); - 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; - } + ProtoSize::add_fixed32_field(total_size, 1, this->epoch_seconds); } +#ifdef USE_API_SERVICES void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name); - buffer.encode_enum(2, this->type); + buffer.encode_uint32(2, static_cast(this->type)); } void ListEntitiesServicesArgument::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->type), false); -} -bool ListEntitiesServicesResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->name = value.as_string(); - return true; - } - case 3: { - this->args.push_back(value.as_message()); - return true; - } - default: - return false; - } -} -bool ListEntitiesServicesResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->name); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->type)); } void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { - buffer.encode_message(3, it, true); + buffer.encode_message(3, it, true); } } void ListEntitiesServicesResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); + ProtoSize::add_string_field(total_size, 1, this->name); + 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_, false); - ProtoSize::add_int32_field(total_size, 1, this->legacy_int, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->float_ != 0.0f, false); - ProtoSize::add_string_field(total_size, 1, this->string_, false); - ProtoSize::add_sint32_field(total_size, 1, this->int_, false); - if (!this->bool_array.empty()) { - for (const auto it : this->bool_array) { - ProtoSize::add_bool_field(total_size, 1, it, true); - } - } - if (!this->int_array.empty()) { - for (const auto &it : this->int_array) { - ProtoSize::add_sint32_field(total_size, 1, it, true); - } - } - if (!this->float_array.empty()) { - for (const auto &it : this->float_array) { - ProtoSize::add_fixed_field<4>(total_size, 1, it != 0.0f, true); - } - } - if (!this->string_array.empty()) { - for (const auto &it : this->string_array) { - ProtoSize::add_string_field(total_size, 1, it, true); - } - } + return true; } bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { - this->args.push_back(value.as_message()); - return true; - } + case 2: + this->args.emplace_back(); + value.decode_to_message(this->args.back()); + 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; } -void ExecuteServiceRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - for (auto &it : this->args) { - buffer.encode_message(2, it, true); - } -} -void ExecuteServiceRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_repeated_message(total_size, 1, this->args); -} +#endif #ifdef USE_CAMERA -bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 5: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 8: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesCameraResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 6: { - this->icon = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesCameraResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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); - buffer.encode_enum(7, this->entity_category); +#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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool CameraImageResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 3: { - this->done = value.as_bool(); - return true; - } - default: - return false; - } -} -bool CameraImageResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->data = value.as_string(); - return true; - } - default: - return false; - } -} -bool CameraImageResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); + 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, false); - ProtoSize::add_string_field(total_size, 1, this->data, false); - ProtoSize::add_bool_field(total_size, 1, this->done, false); + 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; } -} -void CameraImageRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_bool(1, this->single); - buffer.encode_bool(2, this->stream); -} -void CameraImageRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_bool_field(total_size, 1, this->single, false); - ProtoSize::add_bool_field(total_size, 1, this->stream, false); + return true; } #endif #ifdef USE_CLIMATE -bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 5: { - this->supports_current_temperature = value.as_bool(); - return true; - } - case 6: { - this->supports_two_point_target_temperature = value.as_bool(); - return true; - } - case 7: { - this->supported_modes.push_back(value.as_enum()); - return true; - } - case 11: { - this->legacy_supports_away = value.as_bool(); - return true; - } - case 12: { - this->supports_action = value.as_bool(); - return true; - } - case 13: { - this->supported_fan_modes.push_back(value.as_enum()); - return true; - } - case 14: { - this->supported_swing_modes.push_back(value.as_enum()); - return true; - } - case 16: { - this->supported_presets.push_back(value.as_enum()); - return true; - } - case 18: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 20: { - this->entity_category = value.as_enum(); - return true; - } - case 22: { - this->supports_current_humidity = value.as_bool(); - return true; - } - case 23: { - this->supports_target_humidity = value.as_bool(); - return true; - } - case 26: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesClimateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 15: { - this->supported_custom_fan_modes.push_back(value.as_string()); - return true; - } - case 17: { - this->supported_custom_presets.push_back(value.as_string()); - return true; - } - case 19: { - this->icon = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - case 8: { - this->visual_min_temperature = value.as_float(); - return true; - } - case 9: { - this->visual_max_temperature = value.as_float(); - return true; - } - case 10: { - this->visual_target_temperature_step = value.as_float(); - return true; - } - case 21: { - this->visual_current_temperature_step = value.as_float(); - return true; - } - case 24: { - this->visual_min_humidity = value.as_float(); - return true; - } - case 25: { - this->visual_max_humidity = value.as_float(); - return true; - } - default: - return false; - } -} 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) { - buffer.encode_enum(7, it, true); + buffer.encode_uint32(7, static_cast(it), true); } buffer.encode_float(8, this->visual_min_temperature); buffer.encode_float(9, this->visual_max_temperature); buffer.encode_float(10, this->visual_target_temperature_step); - buffer.encode_bool(11, this->legacy_supports_away); buffer.encode_bool(12, this->supports_action); for (auto &it : this->supported_fan_modes) { - buffer.encode_enum(13, it, true); + buffer.encode_uint32(13, static_cast(it), true); } for (auto &it : this->supported_swing_modes) { - buffer.encode_enum(14, it, true); + buffer.encode_uint32(14, static_cast(it), true); } for (auto &it : this->supported_custom_fan_modes) { buffer.encode_string(15, it, true); } for (auto &it : this->supported_presets) { - buffer.encode_enum(16, it, true); + buffer.encode_uint32(16, static_cast(it), true); } for (auto &it : this->supported_custom_presets) { buffer.encode_string(17, it, true); } buffer.encode_bool(18, this->disabled_by_default); +#ifdef USE_ENTITY_ICON buffer.encode_string(19, this->icon); - buffer.encode_enum(20, this->entity_category); +#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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_bool_field(total_size, 1, this->supports_current_temperature, false); - ProtoSize::add_bool_field(total_size, 1, this->supports_two_point_target_temperature, false); + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); + 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()) { for (const auto &it : this->supported_modes) { - ProtoSize::add_enum_field(total_size, 1, static_cast(it), true); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_max_temperature != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_target_temperature_step != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_away, false); - ProtoSize::add_bool_field(total_size, 1, this->supports_action, false); + 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->supports_action); if (!this->supported_fan_modes.empty()) { for (const auto &it : this->supported_fan_modes) { - ProtoSize::add_enum_field(total_size, 1, static_cast(it), true); + ProtoSize::add_enum_field_repeated(total_size, 1, static_cast(it)); } } if (!this->supported_swing_modes.empty()) { for (const auto &it : this->supported_swing_modes) { - ProtoSize::add_enum_field(total_size, 1, static_cast(it), true); + ProtoSize::add_enum_field_repeated(total_size, 1, static_cast(it)); } } if (!this->supported_custom_fan_modes.empty()) { for (const auto &it : this->supported_custom_fan_modes) { - ProtoSize::add_string_field(total_size, 1, it, true); + ProtoSize::add_string_field_repeated(total_size, 1, it); } } if (!this->supported_presets.empty()) { for (const auto &it : this->supported_presets) { - ProtoSize::add_enum_field(total_size, 2, static_cast(it), true); + ProtoSize::add_enum_field_repeated(total_size, 2, static_cast(it)); } } if (!this->supported_custom_presets.empty()) { for (const auto &it : this->supported_custom_presets) { - ProtoSize::add_string_field(total_size, 2, it, true); + ProtoSize::add_string_field_repeated(total_size, 2, it); } } - ProtoSize::add_bool_field(total_size, 2, this->disabled_by_default, false); - ProtoSize::add_string_field(total_size, 2, this->icon, false); - ProtoSize::add_enum_field(total_size, 2, static_cast(this->entity_category), false); - ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_current_temperature_step != 0.0f, false); - ProtoSize::add_bool_field(total_size, 2, this->supports_current_humidity, false); - ProtoSize::add_bool_field(total_size, 2, this->supports_target_humidity, false); - ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_min_humidity != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_max_humidity != 0.0f, false); - ProtoSize::add_uint32_field(total_size, 2, this->device_id, false); -} -bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->mode = value.as_enum(); - return true; - } - case 7: { - this->unused_legacy_away = value.as_bool(); - return true; - } - case 8: { - this->action = value.as_enum(); - return true; - } - case 9: { - this->fan_mode = value.as_enum(); - return true; - } - case 10: { - this->swing_mode = value.as_enum(); - return true; - } - case 12: { - this->preset = value.as_enum(); - return true; - } - case 16: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ClimateStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 11: { - this->custom_fan_mode = value.as_string(); - return true; - } - case 13: { - this->custom_preset = value.as_string(); - return true; - } - default: - return false; - } -} -bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - case 3: { - this->current_temperature = value.as_float(); - return true; - } - case 4: { - this->target_temperature = value.as_float(); - return true; - } - case 5: { - this->target_temperature_low = value.as_float(); - return true; - } - case 6: { - this->target_temperature_high = value.as_float(); - return true; - } - case 14: { - this->current_humidity = value.as_float(); - return true; - } - case 15: { - this->target_humidity = value.as_float(); - return true; - } - default: - return false; - } + 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_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_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); - buffer.encode_enum(2, this->mode); + buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_float(3, this->current_temperature); buffer.encode_float(4, this->target_temperature); buffer.encode_float(5, this->target_temperature_low); buffer.encode_float(6, this->target_temperature_high); - buffer.encode_bool(7, this->unused_legacy_away); - buffer.encode_enum(8, this->action); - buffer.encode_enum(9, this->fan_mode); - buffer.encode_enum(10, this->swing_mode); + buffer.encode_uint32(8, static_cast(this->action)); + buffer.encode_uint32(9, static_cast(this->fan_mode)); + buffer.encode_uint32(10, static_cast(this->swing_mode)); buffer.encode_string(11, this->custom_fan_mode); - buffer.encode_enum(12, this->preset); + buffer.encode_uint32(12, static_cast(this->preset)); 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, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->current_temperature != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_low != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_high != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->unused_legacy_away, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->action), false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->fan_mode), false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->swing_mode), false); - ProtoSize::add_string_field(total_size, 1, this->custom_fan_mode, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->preset), false); - ProtoSize::add_string_field(total_size, 1, this->custom_preset, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->current_humidity != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_humidity != 0.0f, false); - ProtoSize::add_uint32_field(total_size, 2, this->device_id, false); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode)); + 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_enum_field(total_size, 1, static_cast(this->action)); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->fan_mode)); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->swing_mode)); + 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_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: { - this->mode = value.as_enum(); - return true; - } - case 4: { + break; + case 3: + this->mode = static_cast(value.as_uint32()); + 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: { - this->unused_has_legacy_away = value.as_bool(); - return true; - } - 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: { - this->fan_mode = value.as_enum(); - return true; - } - case 14: { + break; + case 13: + this->fan_mode = static_cast(value.as_uint32()); + break; + case 14: this->has_swing_mode = value.as_bool(); - return true; - } - case 15: { - this->swing_mode = value.as_enum(); - return true; - } - case 16: { + break; + case 15: + this->swing_mode = static_cast(value.as_uint32()); + 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: { - this->preset = value.as_enum(); - return true; - } - case 20: { + break; + case 19: + this->preset = static_cast(value.as_uint32()); + 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; - } + break; +#ifdef USE_DEVICES + case 24: + this->device_id = value.as_uint32(); + 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; } -} -void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_bool(2, this->has_mode); - buffer.encode_enum(3, this->mode); - buffer.encode_bool(4, this->has_target_temperature); - buffer.encode_float(5, this->target_temperature); - buffer.encode_bool(6, this->has_target_temperature_low); - buffer.encode_float(7, this->target_temperature_low); - buffer.encode_bool(8, this->has_target_temperature_high); - buffer.encode_float(9, this->target_temperature_high); - buffer.encode_bool(10, this->unused_has_legacy_away); - buffer.encode_bool(11, this->unused_legacy_away); - buffer.encode_bool(12, this->has_fan_mode); - buffer.encode_enum(13, this->fan_mode); - buffer.encode_bool(14, this->has_swing_mode); - buffer.encode_enum(15, this->swing_mode); - buffer.encode_bool(16, this->has_custom_fan_mode); - buffer.encode_string(17, this->custom_fan_mode); - buffer.encode_bool(18, this->has_preset); - buffer.encode_enum(19, this->preset); - buffer.encode_bool(20, this->has_custom_preset); - buffer.encode_string(21, this->custom_preset); - buffer.encode_bool(22, this->has_target_humidity); - buffer.encode_float(23, this->target_humidity); -} -void ClimateCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_bool_field(total_size, 1, this->has_mode, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); - ProtoSize::add_bool_field(total_size, 1, this->has_target_temperature, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->has_target_temperature_low, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_low != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->has_target_temperature_high, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_high != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->unused_has_legacy_away, false); - ProtoSize::add_bool_field(total_size, 1, this->unused_legacy_away, false); - ProtoSize::add_bool_field(total_size, 1, this->has_fan_mode, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->fan_mode), false); - ProtoSize::add_bool_field(total_size, 1, this->has_swing_mode, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->swing_mode), false); - ProtoSize::add_bool_field(total_size, 2, this->has_custom_fan_mode, false); - ProtoSize::add_string_field(total_size, 2, this->custom_fan_mode, false); - ProtoSize::add_bool_field(total_size, 2, this->has_preset, false); - ProtoSize::add_enum_field(total_size, 2, static_cast(this->preset), false); - ProtoSize::add_bool_field(total_size, 2, this->has_custom_preset, false); - ProtoSize::add_string_field(total_size, 2, this->custom_preset, false); - ProtoSize::add_bool_field(total_size, 2, this->has_target_humidity, false); - ProtoSize::add_fixed_field<4>(total_size, 2, this->target_humidity != 0.0f, false); + return true; } #endif #ifdef USE_NUMBER -bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 9: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 10: { - this->entity_category = value.as_enum(); - return true; - } - case 12: { - this->mode = value.as_enum(); - return true; - } - case 14: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesNumberResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 11: { - this->unit_of_measurement = value.as_string(); - return true; - } - case 13: { - this->device_class = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesNumberResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - case 6: { - this->min_value = value.as_float(); - return true; - } - case 7: { - this->max_value = value.as_float(); - return true; - } - case 8: { - this->step = value.as_float(); - return true; - } - default: - return false; - } -} 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); buffer.encode_bool(9, this->disabled_by_default); - buffer.encode_enum(10, this->entity_category); + buffer.encode_uint32(10, static_cast(this->entity_category)); buffer.encode_string(11, this->unit_of_measurement); - buffer.encode_enum(12, this->mode); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->min_value != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->max_value != 0.0f, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->step != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); - ProtoSize::add_string_field(total_size, 1, this->device_class, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool NumberStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 3: { - this->missing_state = value.as_bool(); - return true; - } - case 4: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool NumberStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - case 2: { - this->state = value.as_float(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#ifdef USE_ENTITY_ICON + ProtoSize::add_string_field(total_size, 1, this->icon); +#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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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) { +#ifdef USE_DEVICES + case 3: + this->device_id = value.as_uint32(); + 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; } -} -void NumberCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_float(2, this->state); -} -void NumberCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f, false); + return true; } #endif #ifdef USE_SELECT -bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 7: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 8: { - this->entity_category = value.as_enum(); - return true; - } - case 9: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesSelectResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 6: { - this->options.push_back(value.as_string()); - return true; - } - default: - return false; - } -} -bool ListEntitiesSelectResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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_enum(8, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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(total_size, 1, it, true); + ProtoSize::add_string_field_repeated(total_size, 1, it); } } - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool SelectStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 3: { - this->missing_state = value.as_bool(); - return true; - } - case 4: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool SelectStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->state = value.as_string(); - return true; - } - default: - return false; - } -} -bool SelectStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + 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, false); - ProtoSize::add_string_field(total_size, 1, this->state, false); - ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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) { +#ifdef USE_DEVICES + case 3: + this->device_id = value.as_uint32(); + 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; } -} -void SelectCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_string(2, this->state); -} -void SelectCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->state, false); + return true; } #endif #ifdef USE_SIREN -bool ListEntitiesSirenResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 8: { - this->supports_duration = value.as_bool(); - return true; - } - case 9: { - this->supports_volume = value.as_bool(); - return true; - } - case 10: { - this->entity_category = value.as_enum(); - return true; - } - case 11: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesSirenResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 7: { - this->tones.push_back(value.as_string()); - return true; - } - default: - return false; - } -} -bool ListEntitiesSirenResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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); } buffer.encode_bool(8, this->supports_duration); buffer.encode_bool(9, this->supports_volume); - buffer.encode_enum(10, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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) { - ProtoSize::add_string_field(total_size, 1, it, true); + ProtoSize::add_string_field_repeated(total_size, 1, it); } } - ProtoSize::add_bool_field(total_size, 1, this->supports_duration, false); - ProtoSize::add_bool_field(total_size, 1, this->supports_volume, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool SirenStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->state = value.as_bool(); - return true; - } - case 3: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool SirenStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + 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, false); - ProtoSize::add_bool_field(total_size, 1, this->state, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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; - } + break; +#ifdef USE_DEVICES + case 10: + this->device_id = value.as_uint32(); + 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; } -} -void SirenCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_bool(2, this->has_state); - buffer.encode_bool(3, this->state); - buffer.encode_bool(4, this->has_tone); - buffer.encode_string(5, this->tone); - buffer.encode_bool(6, this->has_duration); - buffer.encode_uint32(7, this->duration); - buffer.encode_bool(8, this->has_volume); - buffer.encode_float(9, this->volume); -} -void SirenCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_bool_field(total_size, 1, this->has_state, false); - ProtoSize::add_bool_field(total_size, 1, this->state, false); - ProtoSize::add_bool_field(total_size, 1, this->has_tone, false); - ProtoSize::add_string_field(total_size, 1, this->tone, false); - ProtoSize::add_bool_field(total_size, 1, this->has_duration, false); - ProtoSize::add_uint32_field(total_size, 1, this->duration, false); - ProtoSize::add_bool_field(total_size, 1, this->has_volume, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false); + return true; } #endif #ifdef USE_LOCK -bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 8: { - this->assumed_state = value.as_bool(); - return true; - } - case 9: { - this->supports_open = value.as_bool(); - return true; - } - case 10: { - this->requires_code = value.as_bool(); - return true; - } - case 12: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesLockResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 11: { - this->code_format = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesLockResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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_enum(7, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false); - ProtoSize::add_bool_field(total_size, 1, this->supports_open, false); - ProtoSize::add_bool_field(total_size, 1, this->requires_code, false); - ProtoSize::add_string_field(total_size, 1, this->code_format, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool LockStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->state = value.as_enum(); - return true; - } - case 3: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool LockStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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_enum(2, this->state); + 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, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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: { - this->command = value.as_enum(); - return true; - } - case 3: { + case 2: + this->command = static_cast(value.as_uint32()); + break; + case 3: this->has_code = value.as_bool(); - return true; - } + break; +#ifdef USE_DEVICES + case 5: + this->device_id = value.as_uint32(); + 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; } -} -void LockCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_enum(2, this->command); - buffer.encode_bool(3, this->has_code); - buffer.encode_string(4, this->code); -} -void LockCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->command), false); - ProtoSize::add_bool_field(total_size, 1, this->has_code, false); - ProtoSize::add_string_field(total_size, 1, this->code, false); + return true; } #endif #ifdef USE_BUTTON -bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 9: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesButtonResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 8: { - this->device_class = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesButtonResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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_enum(7, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_string_field(total_size, 1, this->device_class, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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) { +#ifdef USE_DEVICES + case 2: + this->device_id = value.as_uint32(); + 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; } -} -void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); } -void ButtonCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, 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 = value.as_enum(); - 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); buffer.encode_uint32(3, this->num_channels); - buffer.encode_enum(4, this->purpose); + buffer.encode_uint32(4, static_cast(this->purpose)); buffer.encode_uint32(5, this->sample_bytes); } void MediaPlayerSupportedFormat::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->format, false); - ProtoSize::add_uint32_field(total_size, 1, this->sample_rate, false); - ProtoSize::add_uint32_field(total_size, 1, this->num_channels, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->purpose), false); - ProtoSize::add_uint32_field(total_size, 1, this->sample_bytes, false); -} -bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 8: { - this->supports_pause = value.as_bool(); - return true; - } - case 10: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 9: { - this->supported_formats.push_back(value.as_message()); - return true; - } - default: - return false; - } -} -bool ListEntitiesMediaPlayerResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->format); + ProtoSize::add_uint32_field(total_size, 1, this->sample_rate); + ProtoSize::add_uint32_field(total_size, 1, this->num_channels); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->purpose)); + ProtoSize::add_uint32_field(total_size, 1, this->sample_bytes); } 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_enum(7, this->entity_category); + 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); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_bool_field(total_size, 1, this->supports_pause, false); + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->state = value.as_enum(); - return true; - } - case 4: { - this->muted = value.as_bool(); - return true; - } - case 5: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool MediaPlayerStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - case 3: { - this->volume = value.as_float(); - return true; - } - default: - return false; - } +#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_enum(2, this->state); + 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, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->muted, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->state)); + 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: { - this->command = value.as_enum(); - return true; - } - case 4: { + break; + case 3: + this->command = static_cast(value.as_uint32()); + 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; - } + break; +#ifdef USE_DEVICES + case 10: + this->device_id = value.as_uint32(); + 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; } -} -void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_bool(2, this->has_command); - buffer.encode_enum(3, this->command); - buffer.encode_bool(4, this->has_volume); - buffer.encode_float(5, this->volume); - buffer.encode_bool(6, this->has_media_url); - buffer.encode_string(7, this->media_url); - buffer.encode_bool(8, this->has_announcement); - buffer.encode_bool(9, this->announcement); -} -void MediaPlayerCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_bool_field(total_size, 1, this->has_command, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->command), false); - ProtoSize::add_bool_field(total_size, 1, this->has_volume, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->has_media_url, false); - ProtoSize::add_string_field(total_size, 1, this->media_url, false); - ProtoSize::add_bool_field(total_size, 1, this->has_announcement, false); - ProtoSize::add_bool_field(total_size, 1, this->announcement, 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; - } -} -void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint32(1, this->flags); -} -void SubscribeBluetoothLEAdvertisementsRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint32_field(total_size, 1, this->flags, 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; - } - default: - return false; - } -} -void BluetoothServiceData::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->uuid); - for (auto &it : this->legacy_data) { - buffer.encode_uint32(2, it, true); - } - buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size()); -} -void BluetoothServiceData::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->uuid, false); - if (!this->legacy_data.empty()) { - for (const auto &it : this->legacy_data) { - ProtoSize::add_uint32_field(total_size, 1, it, true); - } - } - ProtoSize::add_string_field(total_size, 1, this->data, false); -} -bool BluetoothLEAdvertisementResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - case 3: { - this->rssi = value.as_sint32(); - return true; - } - case 7: { - this->address_type = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool BluetoothLEAdvertisementResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->name = value.as_string(); - return true; - } - case 4: { - this->service_uuids.push_back(value.as_string()); - return true; - } - case 5: { - this->service_data.push_back(value.as_message()); - return true; - } - case 6: { - this->manufacturer_data.push_back(value.as_message()); - return true; - } - default: - return false; - } -} -void BluetoothLEAdvertisementResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_bytes(2, reinterpret_cast(this->name.data()), this->name.size()); - buffer.encode_sint32(3, this->rssi); - for (auto &it : this->service_uuids) { - buffer.encode_string(4, it, true); - } - for (auto &it : this->service_data) { - buffer.encode_message(5, it, true); - } - for (auto &it : this->manufacturer_data) { - buffer.encode_message(6, it, true); - } - buffer.encode_uint32(7, this->address_type); -} -void BluetoothLEAdvertisementResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_sint32_field(total_size, 1, this->rssi, false); - if (!this->service_uuids.empty()) { - for (const auto &it : this->service_uuids) { - ProtoSize::add_string_field(total_size, 1, it, true); - } - } - ProtoSize::add_repeated_message(total_size, 1, this->service_data); - ProtoSize::add_repeated_message(total_size, 1, this->manufacturer_data); - ProtoSize::add_uint32_field(total_size, 1, this->address_type, false); -} -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; - } + break; default: return false; } + return true; } 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, false); - ProtoSize::add_sint32_field(total_size, 1, this->rssi, false); - ProtoSize::add_uint32_field(total_size, 1, this->address_type, false); - ProtoSize::add_string_field(total_size, 1, this->data, false); -} -bool BluetoothLERawAdvertisementsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->advertisements.push_back(value.as_message()); - return true; - } - default: - return false; + 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); + 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) { - buffer.encode_message(1, it, true); + buffer.encode_message(1, it, true); } } void BluetoothLERawAdvertisementsResponse::calculate_size(uint32_t &total_size) const { @@ -4085,59 +1853,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: { - this->request_type = value.as_enum(); - return true; - } - case 3: { + break; + case 2: + this->request_type = static_cast(value.as_uint32()); + 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; - } - default: - return false; - } -} -void BluetoothDeviceRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_enum(2, this->request_type); - buffer.encode_bool(3, this->has_address_type); - buffer.encode_uint32(4, this->address_type); -} -void BluetoothDeviceRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->request_type), false); - ProtoSize::add_bool_field(total_size, 1, this->has_address_type, false); - ProtoSize::add_uint32_field(total_size, 1, this->address_type, false); -} -bool BluetoothDeviceConnectionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - case 2: { - this->connected = value.as_bool(); - return true; - } - case 3: { - this->mtu = value.as_uint32(); - return true; - } - case 4: { - this->error = value.as_int32(); - return true; - } + break; default: return false; } + return true; } void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -4146,38 +1877,20 @@ void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_int32(4, this->error); } void BluetoothDeviceConnectionResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_bool_field(total_size, 1, this->connected, false); - ProtoSize::add_uint32_field(total_size, 1, this->mtu, false); - ProtoSize::add_int32_field(total_size, 1, this->error, false); + ProtoSize::add_uint64_field(total_size, 1, this->address); + ProtoSize::add_bool_field(total_size, 1, this->connected); + ProtoSize::add_uint32_field(total_size, 1, this->mtu); + ProtoSize::add_int32_field(total_size, 1, this->error); } 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; - } -} -void BluetoothGATTGetServicesRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); } -void BluetoothGATTGetServicesRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, 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) { @@ -4188,38 +1901,10 @@ void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { void BluetoothGATTDescriptor::calculate_size(uint32_t &total_size) const { if (!this->uuid.empty()) { for (const auto &it : this->uuid) { - ProtoSize::add_uint64_field(total_size, 1, it, true); + ProtoSize::add_uint64_field_repeated(total_size, 1, it); } } - ProtoSize::add_uint32_field(total_size, 1, this->handle, false); -} -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.push_back(value.as_message()); - return true; - } - default: - return false; - } + ProtoSize::add_uint32_field(total_size, 1, this->handle); } void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->uuid) { @@ -4228,152 +1913,65 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->properties); for (auto &it : this->descriptors) { - buffer.encode_message(4, it, true); + buffer.encode_message(4, it, true); } } void BluetoothGATTCharacteristic::calculate_size(uint32_t &total_size) const { if (!this->uuid.empty()) { for (const auto &it : this->uuid) { - ProtoSize::add_uint64_field(total_size, 1, it, true); + ProtoSize::add_uint64_field_repeated(total_size, 1, it); } } - ProtoSize::add_uint32_field(total_size, 1, this->handle, false); - ProtoSize::add_uint32_field(total_size, 1, this->properties, false); + ProtoSize::add_uint32_field(total_size, 1, this->handle); + 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.push_back(value.as_message()); - return true; - } - default: - return false; - } -} void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->uuid) { buffer.encode_uint64(1, it, true); } buffer.encode_uint32(2, this->handle); for (auto &it : this->characteristics) { - buffer.encode_message(3, it, true); + buffer.encode_message(3, it, true); } } void BluetoothGATTService::calculate_size(uint32_t &total_size) const { if (!this->uuid.empty()) { for (const auto &it : this->uuid) { - ProtoSize::add_uint64_field(total_size, 1, it, true); + ProtoSize::add_uint64_field_repeated(total_size, 1, it); } } - ProtoSize::add_uint32_field(total_size, 1, this->handle, false); + ProtoSize::add_uint32_field(total_size, 1, this->handle); ProtoSize::add_repeated_message(total_size, 1, this->characteristics); } -bool BluetoothGATTGetServicesResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - default: - return false; - } -} -bool BluetoothGATTGetServicesResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->services.push_back(value.as_message()); - return true; - } - default: - return false; - } -} void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); for (auto &it : this->services) { - buffer.encode_message(2, it, true); + buffer.encode_message(2, it, true); } } void BluetoothGATTGetServicesResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_uint64_field(total_size, 1, this->address); ProtoSize::add_repeated_message(total_size, 1, this->services); } -bool BluetoothGATTGetServicesDoneResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - default: - return false; - } -} void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); } void BluetoothGATTGetServicesDoneResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); + ProtoSize::add_uint64_field(total_size, 1, this->address); } 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; - } - default: - return false; - } -} -void BluetoothGATTReadRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); -} -void BluetoothGATTReadRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_uint32_field(total_size, 1, this->handle, false); -} -bool BluetoothGATTReadResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - case 2: { - this->handle = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool BluetoothGATTReadResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 3: { - this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -4381,157 +1979,87 @@ void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size()); } void BluetoothGATTReadResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_uint32_field(total_size, 1, this->handle, false); - ProtoSize::add_string_field(total_size, 1, this->data, false); + ProtoSize::add_uint64_field(total_size, 1, this->address); + ProtoSize::add_uint32_field(total_size, 1, this->handle); + ProtoSize::add_string_field(total_size, 1, this->data); } 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; } -} -void BluetoothGATTWriteRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); - buffer.encode_bool(3, this->response); - buffer.encode_bytes(4, reinterpret_cast(this->data.data()), this->data.size()); -} -void BluetoothGATTWriteRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_uint32_field(total_size, 1, this->handle, false); - ProtoSize::add_bool_field(total_size, 1, this->response, false); - ProtoSize::add_string_field(total_size, 1, this->data, 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; } -} -void BluetoothGATTReadDescriptorRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); -} -void BluetoothGATTReadDescriptorRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_uint32_field(total_size, 1, this->handle, 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; } -} -void BluetoothGATTWriteDescriptorRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); - buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size()); -} -void BluetoothGATTWriteDescriptorRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_uint32_field(total_size, 1, this->handle, false); - ProtoSize::add_string_field(total_size, 1, this->data, 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; - } - default: - return false; - } -} -void BluetoothGATTNotifyRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); - buffer.encode_bool(3, this->enable); -} -void BluetoothGATTNotifyRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_uint32_field(total_size, 1, this->handle, false); - ProtoSize::add_bool_field(total_size, 1, this->enable, false); -} -bool BluetoothGATTNotifyDataResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - case 2: { - this->handle = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool BluetoothGATTNotifyDataResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 3: { - this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -4539,27 +2067,9 @@ void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size()); } void BluetoothGATTNotifyDataResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_uint32_field(total_size, 1, this->handle, false); - ProtoSize::add_string_field(total_size, 1, this->data, false); -} -bool BluetoothConnectionsFreeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->free = value.as_uint32(); - return true; - } - case 2: { - this->limit = value.as_uint32(); - return true; - } - case 3: { - this->allocated.push_back(value.as_uint64()); - return true; - } - default: - return false; - } + ProtoSize::add_uint64_field(total_size, 1, this->address); + ProtoSize::add_uint32_field(total_size, 1, this->handle); + ProtoSize::add_string_field(total_size, 1, this->data); } void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->free); @@ -4569,103 +2079,39 @@ void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { } } void BluetoothConnectionsFreeResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint32_field(total_size, 1, this->free, false); - ProtoSize::add_uint32_field(total_size, 1, this->limit, false); + ProtoSize::add_uint32_field(total_size, 1, this->free); + ProtoSize::add_uint32_field(total_size, 1, this->limit); if (!this->allocated.empty()) { for (const auto &it : this->allocated) { - ProtoSize::add_uint64_field(total_size, 1, it, true); + ProtoSize::add_uint64_field_repeated(total_size, 1, it); } } } -bool BluetoothGATTErrorResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - case 2: { - this->handle = value.as_uint32(); - return true; - } - case 3: { - this->error = value.as_int32(); - return true; - } - default: - return false; - } -} void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_int32(3, this->error); } void BluetoothGATTErrorResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_uint32_field(total_size, 1, this->handle, false); - ProtoSize::add_int32_field(total_size, 1, this->error, false); -} -bool BluetoothGATTWriteResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - case 2: { - this->handle = value.as_uint32(); - return true; - } - default: - return false; - } + ProtoSize::add_uint64_field(total_size, 1, this->address); + ProtoSize::add_uint32_field(total_size, 1, this->handle); + ProtoSize::add_int32_field(total_size, 1, this->error); } void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } void BluetoothGATTWriteResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_uint32_field(total_size, 1, this->handle, false); -} -bool BluetoothGATTNotifyResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - case 2: { - this->handle = value.as_uint32(); - return true; - } - default: - return false; - } + ProtoSize::add_uint64_field(total_size, 1, this->address); + ProtoSize::add_uint32_field(total_size, 1, this->handle); } void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } void BluetoothGATTNotifyResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_uint32_field(total_size, 1, this->handle, false); -} -bool BluetoothDevicePairingResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - case 2: { - this->paired = value.as_bool(); - return true; - } - case 3: { - this->error = value.as_int32(); - return true; - } - default: - return false; - } + ProtoSize::add_uint64_field(total_size, 1, this->address); + ProtoSize::add_uint32_field(total_size, 1, this->handle); } void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -4673,27 +2119,9 @@ void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_int32(3, this->error); } void BluetoothDevicePairingResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_bool_field(total_size, 1, this->paired, false); - ProtoSize::add_int32_field(total_size, 1, this->error, false); -} -bool BluetoothDeviceUnpairingResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - case 2: { - this->success = value.as_bool(); - return true; - } - case 3: { - this->error = value.as_int32(); - return true; - } - default: - return false; - } + ProtoSize::add_uint64_field(total_size, 1, this->address); + ProtoSize::add_bool_field(total_size, 1, this->paired); + ProtoSize::add_int32_field(total_size, 1, this->error); } void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -4701,27 +2129,9 @@ void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_int32(3, this->error); } void BluetoothDeviceUnpairingResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_bool_field(total_size, 1, this->success, false); - ProtoSize::add_int32_field(total_size, 1, this->error, false); -} -bool BluetoothDeviceClearCacheResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->address = value.as_uint64(); - return true; - } - case 2: { - this->success = value.as_bool(); - return true; - } - case 3: { - this->error = value.as_int32(); - return true; - } - default: - return false; - } + ProtoSize::add_uint64_field(total_size, 1, this->address); + ProtoSize::add_bool_field(total_size, 1, this->success); + ProtoSize::add_int32_field(total_size, 1, this->error); } void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -4729,95 +2139,42 @@ void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_int32(3, this->error); } void BluetoothDeviceClearCacheResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address, false); - ProtoSize::add_bool_field(total_size, 1, this->success, false); - ProtoSize::add_int32_field(total_size, 1, this->error, false); -} -bool BluetoothScannerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->state = value.as_enum(); - return true; - } - case 2: { - this->mode = value.as_enum(); - return true; - } - default: - return false; - } + ProtoSize::add_uint64_field(total_size, 1, this->address); + ProtoSize::add_bool_field(total_size, 1, this->success); + ProtoSize::add_int32_field(total_size, 1, this->error); } void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_enum(1, this->state); - buffer.encode_enum(2, this->mode); + buffer.encode_uint32(1, static_cast(this->state)); + buffer.encode_uint32(2, static_cast(this->mode)); } void BluetoothScannerStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->state)); + ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode)); } bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { - this->mode = value.as_enum(); - return true; - } + case 1: + this->mode = static_cast(value.as_uint32()); + break; default: return false; } -} -void BluetoothScannerSetModeRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_enum(1, this->mode); -} -void BluetoothScannerSetModeRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), 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; - } -} -void SubscribeVoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_bool(1, this->subscribe); - buffer.encode_uint32(2, this->flags); -} -void SubscribeVoiceAssistantRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_bool_field(total_size, 1, this->subscribe, false); - ProtoSize::add_uint32_field(total_size, 1, this->flags, 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); @@ -4825,281 +2182,160 @@ void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(3, this->volume_multiplier); } void VoiceAssistantAudioSettings::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint32_field(total_size, 1, this->noise_suppression_level, false); - ProtoSize::add_uint32_field(total_size, 1, this->auto_gain, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->volume_multiplier != 0.0f, false); -} -bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->start = value.as_bool(); - return true; - } - case 3: { - this->flags = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->conversation_id = value.as_string(); - return true; - } - case 4: { - this->audio_settings = value.as_message(); - return true; - } - case 5: { - this->wake_word_phrase = value.as_string(); - return true; - } - default: - return false; - } + ProtoSize::add_uint32_field(total_size, 1, this->noise_suppression_level); + ProtoSize::add_uint32_field(total_size, 1, this->auto_gain); + ProtoSize::add_float_field(total_size, 1, this->volume_multiplier); } void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->start); buffer.encode_string(2, this->conversation_id); buffer.encode_uint32(3, this->flags); - buffer.encode_message(4, this->audio_settings); + buffer.encode_message(4, this->audio_settings); buffer.encode_string(5, this->wake_word_phrase); } void VoiceAssistantRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_bool_field(total_size, 1, this->start, false); - ProtoSize::add_string_field(total_size, 1, this->conversation_id, false); - ProtoSize::add_uint32_field(total_size, 1, this->flags, false); - ProtoSize::add_message_object(total_size, 1, this->audio_settings, false); - ProtoSize::add_string_field(total_size, 1, this->wake_word_phrase, false); + ProtoSize::add_bool_field(total_size, 1, this->start); + ProtoSize::add_string_field(total_size, 1, this->conversation_id); + ProtoSize::add_uint32_field(total_size, 1, this->flags); + ProtoSize::add_message_object(total_size, 1, this->audio_settings); + ProtoSize::add_string_field(total_size, 1, this->wake_word_phrase); } 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; } -} -void VoiceAssistantResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint32(1, this->port); - buffer.encode_bool(2, this->error); -} -void VoiceAssistantResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint32_field(total_size, 1, this->port, false); - ProtoSize::add_bool_field(total_size, 1, this->error, 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, false); - ProtoSize::add_string_field(total_size, 1, this->value, false); + return true; } bool VoiceAssistantEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { - this->event_type = value.as_enum(); - return true; - } + case 1: + this->event_type = static_cast(value.as_uint32()); + break; default: return false; } + return true; } bool VoiceAssistantEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { - this->data.push_back(value.as_message()); - return true; - } + case 2: + this->data.emplace_back(); + value.decode_to_message(this->data.back()); + break; default: return false; } -} -void VoiceAssistantEventResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_enum(1, this->event_type); - for (auto &it : this->data) { - buffer.encode_message(2, it, true); - } -} -void VoiceAssistantEventResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_enum_field(total_size, 1, static_cast(this->event_type), false); - ProtoSize::add_repeated_message(total_size, 1, this->data); + 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()); buffer.encode_bool(2, this->end); } void VoiceAssistantAudio::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->data, false); - ProtoSize::add_bool_field(total_size, 1, this->end, false); + ProtoSize::add_string_field(total_size, 1, this->data); + ProtoSize::add_bool_field(total_size, 1, this->end); } bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { - this->event_type = value.as_enum(); - return true; - } - case 4: { + case 1: + this->event_type = static_cast(value.as_uint32()); + 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; } -} -void VoiceAssistantTimerEventResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_enum(1, this->event_type); - buffer.encode_string(2, this->timer_id); - buffer.encode_string(3, this->name); - buffer.encode_uint32(4, this->total_seconds); - buffer.encode_uint32(5, this->seconds_left); - buffer.encode_bool(6, this->is_active); -} -void VoiceAssistantTimerEventResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_enum_field(total_size, 1, static_cast(this->event_type), false); - ProtoSize::add_string_field(total_size, 1, this->timer_id, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_uint32_field(total_size, 1, this->total_seconds, false); - ProtoSize::add_uint32_field(total_size, 1, this->seconds_left, false); - ProtoSize::add_bool_field(total_size, 1, this->is_active, 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; - } - default: - return false; - } -} -void VoiceAssistantAnnounceRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->media_id); - buffer.encode_string(2, this->text); - buffer.encode_string(3, this->preannounce_media_id); - buffer.encode_bool(4, this->start_conversation); -} -void VoiceAssistantAnnounceRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->media_id, false); - ProtoSize::add_string_field(total_size, 1, this->text, false); - ProtoSize::add_string_field(total_size, 1, this->preannounce_media_id, false); - ProtoSize::add_bool_field(total_size, 1, this->start_conversation, false); -} -bool VoiceAssistantAnnounceFinished::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->success = value.as_bool(); - 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, false); -} -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; - } + ProtoSize::add_bool_field(total_size, 1, this->success); } void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->id); @@ -5109,41 +2345,17 @@ void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { } } void VoiceAssistantWakeWord::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->id, false); - ProtoSize::add_string_field(total_size, 1, this->wake_word, false); + ProtoSize::add_string_field(total_size, 1, this->id); + ProtoSize::add_string_field(total_size, 1, this->wake_word); if (!this->trained_languages.empty()) { for (const auto &it : this->trained_languages) { - ProtoSize::add_string_field(total_size, 1, it, true); + ProtoSize::add_string_field_repeated(total_size, 1, it); } } } -bool VoiceAssistantConfigurationResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 3: { - this->max_active_wake_words = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool VoiceAssistantConfigurationResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->available_wake_words.push_back(value.as_message()); - return true; - } - case 2: { - this->active_wake_words.push_back(value.as_string()); - return true; - } - default: - return false; - } -} void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->available_wake_words) { - buffer.encode_message(1, it, true); + buffer.encode_message(1, it, true); } for (auto &it : this->active_wake_words) { buffer.encode_string(2, it, true); @@ -5154,474 +2366,215 @@ void VoiceAssistantConfigurationResponse::calculate_size(uint32_t &total_size) c ProtoSize::add_repeated_message(total_size, 1, this->available_wake_words); if (!this->active_wake_words.empty()) { for (const auto &it : this->active_wake_words) { - ProtoSize::add_string_field(total_size, 1, it, true); + ProtoSize::add_string_field_repeated(total_size, 1, it); } } - ProtoSize::add_uint32_field(total_size, 1, this->max_active_wake_words, false); + ProtoSize::add_uint32_field(total_size, 1, this->max_active_wake_words); } 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; } -} -void VoiceAssistantSetConfiguration::encode(ProtoWriteBuffer buffer) const { - for (auto &it : this->active_wake_words) { - buffer.encode_string(1, it, true); - } -} -void VoiceAssistantSetConfiguration::calculate_size(uint32_t &total_size) const { - if (!this->active_wake_words.empty()) { - for (const auto &it : this->active_wake_words) { - ProtoSize::add_string_field(total_size, 1, it, true); - } - } + return true; } #endif #ifdef USE_ALARM_CONTROL_PANEL -bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 8: { - this->supported_features = value.as_uint32(); - return true; - } - case 9: { - this->requires_code = value.as_bool(); - return true; - } - case 10: { - this->requires_code_to_arm = value.as_bool(); - return true; - } - case 11: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesAlarmControlPanelResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesAlarmControlPanelResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} void ListEntitiesAlarmControlPanelResponse::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_enum(7, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_uint32_field(total_size, 1, this->supported_features, false); - ProtoSize::add_bool_field(total_size, 1, this->requires_code, false); - ProtoSize::add_bool_field(total_size, 1, this->requires_code_to_arm, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool AlarmControlPanelStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->state = value.as_enum(); - return true; - } - case 3: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool AlarmControlPanelStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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_enum(2, this->state); + 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, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->state), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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: { - this->command = value.as_enum(); - return true; - } + case 2: + this->command = static_cast(value.as_uint32()); + break; +#ifdef USE_DEVICES + case 4: + this->device_id = value.as_uint32(); + 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; } -} -void AlarmControlPanelCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_enum(2, this->command); - buffer.encode_string(3, this->code); -} -void AlarmControlPanelCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->command), false); - ProtoSize::add_string_field(total_size, 1, this->code, false); + return true; } #endif #ifdef USE_TEXT -bool ListEntitiesTextResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 8: { - this->min_length = value.as_uint32(); - return true; - } - case 9: { - this->max_length = value.as_uint32(); - return true; - } - case 11: { - this->mode = value.as_enum(); - return true; - } - case 12: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesTextResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 10: { - this->pattern = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesTextResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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_enum(7, this->entity_category); + 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_enum(11, this->mode); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_uint32_field(total_size, 1, this->min_length, false); - ProtoSize::add_uint32_field(total_size, 1, this->max_length, false); - ProtoSize::add_string_field(total_size, 1, this->pattern, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool TextStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 3: { - this->missing_state = value.as_bool(); - return true; - } - case 4: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool TextStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->state = value.as_string(); - return true; - } - default: - return false; - } -} -bool TextStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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, false); - ProtoSize::add_string_field(total_size, 1, this->state, false); - ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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) { +#ifdef USE_DEVICES + case 3: + this->device_id = value.as_uint32(); + 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; } -} -void TextCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_string(2, this->state); -} -void TextCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->state, false); + return true; } #endif #ifdef USE_DATETIME_DATE -bool ListEntitiesDateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 8: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesDateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesDateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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_enum(7, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool DateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->missing_state = value.as_bool(); - return true; - } - case 3: { - this->year = value.as_uint32(); - return true; - } - case 4: { - this->month = value.as_uint32(); - return true; - } - case 5: { - this->day = value.as_uint32(); - return true; - } - case 6: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool DateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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); @@ -5629,163 +2582,78 @@ 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, false); - ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); - ProtoSize::add_uint32_field(total_size, 1, this->year, false); - ProtoSize::add_uint32_field(total_size, 1, this->month, false); - ProtoSize::add_uint32_field(total_size, 1, this->day, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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; - } + break; +#ifdef USE_DEVICES + case 5: + this->device_id = value.as_uint32(); + 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; } -} -void DateCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_uint32(2, this->year); - buffer.encode_uint32(3, this->month); - buffer.encode_uint32(4, this->day); -} -void DateCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_uint32_field(total_size, 1, this->year, false); - ProtoSize::add_uint32_field(total_size, 1, this->month, false); - ProtoSize::add_uint32_field(total_size, 1, this->day, false); + return true; } #endif #ifdef USE_DATETIME_TIME -bool ListEntitiesTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 8: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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_enum(7, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool TimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->missing_state = value.as_bool(); - return true; - } - case 3: { - this->hour = value.as_uint32(); - return true; - } - case 4: { - this->minute = value.as_uint32(); - return true; - } - case 5: { - this->second = value.as_uint32(); - return true; - } - case 6: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool TimeStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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); @@ -5793,635 +2661,284 @@ 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, false); - ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); - ProtoSize::add_uint32_field(total_size, 1, this->hour, false); - ProtoSize::add_uint32_field(total_size, 1, this->minute, false); - ProtoSize::add_uint32_field(total_size, 1, this->second, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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; - } + break; +#ifdef USE_DEVICES + case 5: + this->device_id = value.as_uint32(); + 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; } -} -void TimeCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_uint32(2, this->hour); - buffer.encode_uint32(3, this->minute); - buffer.encode_uint32(4, this->second); -} -void TimeCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_uint32_field(total_size, 1, this->hour, false); - ProtoSize::add_uint32_field(total_size, 1, this->minute, false); - ProtoSize::add_uint32_field(total_size, 1, this->second, false); + return true; } #endif #ifdef USE_EVENT -bool ListEntitiesEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 10: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 8: { - this->device_class = value.as_string(); - return true; - } - case 9: { - this->event_types.push_back(value.as_string()); - return true; - } - default: - return false; - } -} -bool ListEntitiesEventResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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_enum(7, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_string_field(total_size, 1, this->device_class, false); + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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); if (!this->event_types.empty()) { for (const auto &it : this->event_types) { - ProtoSize::add_string_field(total_size, 1, it, true); + ProtoSize::add_string_field_repeated(total_size, 1, it); } } - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool EventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 3: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool EventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->event_type = value.as_string(); - return true; - } - default: - return false; - } -} -bool EventResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } +#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, false); - ProtoSize::add_string_field(total_size, 1, this->event_type, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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 -bool ListEntitiesValveResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 9: { - this->assumed_state = value.as_bool(); - return true; - } - case 10: { - this->supports_position = value.as_bool(); - return true; - } - case 11: { - this->supports_stop = value.as_bool(); - return true; - } - case 12: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesValveResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 8: { - this->device_class = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesValveResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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_enum(7, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_string_field(total_size, 1, this->device_class, false); - ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false); - ProtoSize::add_bool_field(total_size, 1, this->supports_position, false); - ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool ValveStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 3: { - this->current_operation = value.as_enum(); - return true; - } - case 4: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ValveStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - case 2: { - this->position = value.as_float(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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_enum(3, this->current_operation); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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; - } + break; +#ifdef USE_DEVICES + case 5: + this->device_id = value.as_uint32(); + 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; } -} -void ValveCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_bool(2, this->has_position); - buffer.encode_float(3, this->position); - buffer.encode_bool(4, this->stop); -} -void ValveCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_bool_field(total_size, 1, this->has_position, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f, false); - ProtoSize::add_bool_field(total_size, 1, this->stop, false); + return true; } #endif #ifdef USE_DATETIME_DATETIME -bool ListEntitiesDateTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 8: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesDateTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesDateTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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_enum(7, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool DateTimeStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->missing_state = value.as_bool(); - return true; - } - case 4: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool DateTimeStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - case 3: { - this->epoch_seconds = value.as_fixed32(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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, false); - ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_bool_field(total_size, 1, this->missing_state); + 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) { +#ifdef USE_DEVICES + case 3: + this->device_id = value.as_uint32(); + 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; } -} -void DateTimeCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_fixed32(2, this->epoch_seconds); -} -void DateTimeCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0, false); + return true; } #endif #ifdef USE_UPDATE -bool ListEntitiesUpdateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 6: { - this->disabled_by_default = value.as_bool(); - return true; - } - case 7: { - this->entity_category = value.as_enum(); - return true; - } - case 9: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool ListEntitiesUpdateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->object_id = value.as_string(); - return true; - } - case 3: { - this->name = value.as_string(); - return true; - } - case 4: { - this->unique_id = value.as_string(); - return true; - } - case 5: { - this->icon = value.as_string(); - return true; - } - case 8: { - this->device_class = value.as_string(); - return true; - } - default: - return false; - } -} -bool ListEntitiesUpdateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 2: { - this->key = value.as_fixed32(); - return true; - } - default: - return false; - } -} 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_enum(7, this->entity_category); + 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, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_string_field(total_size, 1, this->name, false); - ProtoSize::add_string_field(total_size, 1, this->unique_id, false); - ProtoSize::add_string_field(total_size, 1, this->icon, false); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category), false); - ProtoSize::add_string_field(total_size, 1, this->device_class, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); -} -bool UpdateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->missing_state = value.as_bool(); - return true; - } - case 3: { - this->in_progress = value.as_bool(); - return true; - } - case 4: { - this->has_progress = value.as_bool(); - return true; - } - case 11: { - this->device_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool UpdateStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 6: { - this->current_version = value.as_string(); - return true; - } - case 7: { - this->latest_version = value.as_string(); - return true; - } - case 8: { - this->title = value.as_string(); - return true; - } - case 9: { - this->release_summary = value.as_string(); - return true; - } - case 10: { - this->release_url = value.as_string(); - return true; - } - default: - return false; - } -} -bool UpdateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 1: { - this->key = value.as_fixed32(); - return true; - } - case 5: { - this->progress = value.as_float(); - return true; - } - default: - return false; - } + ProtoSize::add_string_field(total_size, 1, this->object_id); + ProtoSize::add_fixed32_field(total_size, 1, this->key); + ProtoSize::add_string_field(total_size, 1, this->name); +#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); @@ -6434,48 +2951,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, false); - ProtoSize::add_bool_field(total_size, 1, this->missing_state, false); - ProtoSize::add_bool_field(total_size, 1, this->in_progress, false); - ProtoSize::add_bool_field(total_size, 1, this->has_progress, false); - ProtoSize::add_fixed_field<4>(total_size, 1, this->progress != 0.0f, false); - ProtoSize::add_string_field(total_size, 1, this->current_version, false); - ProtoSize::add_string_field(total_size, 1, this->latest_version, false); - ProtoSize::add_string_field(total_size, 1, this->title, false); - ProtoSize::add_string_field(total_size, 1, this->release_summary, false); - ProtoSize::add_string_field(total_size, 1, this->release_url, false); - ProtoSize::add_uint32_field(total_size, 1, this->device_id, false); + 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_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: { - this->command = value.as_enum(); - return true; - } + case 2: + this->command = static_cast(value.as_uint32()); + break; +#ifdef USE_DEVICES + case 3: + this->device_id = value.as_uint32(); + 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; } -} -void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); - buffer.encode_enum(2, this->command); -} -void UpdateCommandRequest::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->command), false); + return true; } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 3bfc5f1cf4..7b64bd889f 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -5,7 +5,6 @@ #include "esphome/core/defines.h" #include "proto.h" -#include "api_pb2_size.h" namespace esphome { namespace api { @@ -18,27 +17,13 @@ enum EntityCategory : uint32_t { ENTITY_CATEGORY_DIAGNOSTIC = 2, }; #ifdef USE_COVER -enum LegacyCoverState : uint32_t { - LEGACY_COVER_STATE_OPEN = 0, - LEGACY_COVER_STATE_CLOSED = 1, -}; enum CoverOperation : uint32_t { COVER_OPERATION_IDLE = 0, COVER_OPERATION_IS_OPENING = 1, COVER_OPERATION_IS_CLOSING = 2, }; -enum LegacyCoverCommand : uint32_t { - LEGACY_COVER_COMMAND_OPEN = 0, - LEGACY_COVER_COMMAND_CLOSE = 1, - LEGACY_COVER_COMMAND_STOP = 2, -}; #endif #ifdef USE_FAN -enum FanSpeed : uint32_t { - FAN_SPEED_LOW = 0, - FAN_SPEED_MEDIUM = 1, - FAN_SPEED_HIGH = 2, -}; enum FanDirection : uint32_t { FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_REVERSE = 1, @@ -66,11 +51,6 @@ enum SensorStateClass : uint32_t { STATE_CLASS_TOTAL_INCREASING = 2, STATE_CLASS_TOTAL = 3, }; -enum SensorLastResetType : uint32_t { - LAST_RESET_NONE = 0, - LAST_RESET_NEVER = 1, - LAST_RESET_AUTO = 2, -}; #endif enum LogLevel : uint32_t { LOG_LEVEL_NONE = 0, @@ -82,6 +62,7 @@ enum LogLevel : uint32_t { LOG_LEVEL_VERBOSE = 6, LOG_LEVEL_VERY_VERBOSE = 7, }; +#ifdef USE_API_SERVICES enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_BOOL = 0, SERVICE_ARG_TYPE_INT = 1, @@ -92,6 +73,7 @@ enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_FLOAT_ARRAY = 6, SERVICE_ARG_TYPE_STRING_ARRAY = 7, }; +#endif #ifdef USE_CLIMATE enum ClimateMode : uint32_t { CLIMATE_MODE_OFF = 0, @@ -290,11 +272,14 @@ class InfoResponseProtoMessage : public ProtoMessage { std::string object_id{}; uint32_t key{0}; std::string name{}; - std::string unique_id{}; bool disabled_by_default{false}; +#ifdef USE_ENTITY_ICON std::string icon{}; +#endif enums::EntityCategory entity_category{}; +#ifdef USE_DEVICES uint32_t device_id{0}; +#endif protected: }; @@ -303,22 +288,33 @@ class StateResponseProtoMessage : public ProtoMessage { public: ~StateResponseProtoMessage() override = default; uint32_t key{0}; +#ifdef USE_DEVICES uint32_t device_id{0}; +#endif protected: }; -class HelloRequest : public ProtoMessage { + +class CommandProtoMessage : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 1; - static constexpr uint16_t ESTIMATED_SIZE = 17; + ~CommandProtoMessage() override = default; + uint32_t key{0}; +#ifdef USE_DEVICES + uint32_t device_id{0}; +#endif + + protected: +}; +class HelloRequest : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 1; + static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "hello_request"; } #endif std::string client_info{}; uint32_t api_version_major{0}; uint32_t api_version_minor{0}; - 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 @@ -329,8 +325,8 @@ class HelloRequest : public ProtoMessage { }; class HelloResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 2; - static constexpr uint16_t ESTIMATED_SIZE = 26; + static constexpr uint8_t MESSAGE_TYPE = 2; + static constexpr uint8_t ESTIMATED_SIZE = 26; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "hello_response"; } #endif @@ -345,19 +341,15 @@ class HelloResponse : 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 ConnectRequest : public ProtoMessage { +class ConnectRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 3; - static constexpr uint16_t ESTIMATED_SIZE = 9; + static constexpr uint8_t MESSAGE_TYPE = 3; + static constexpr uint8_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "connect_request"; } #endif std::string password{}; - 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 @@ -367,8 +359,8 @@ class ConnectRequest : public ProtoMessage { }; class ConnectResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 4; - static constexpr uint16_t ESTIMATED_SIZE = 2; + static constexpr uint8_t MESSAGE_TYPE = 4; + static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "connect_response"; } #endif @@ -380,12 +372,11 @@ class ConnectResponse : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class DisconnectRequest : public ProtoMessage { +class DisconnectRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 5; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 5; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "disconnect_request"; } #endif @@ -395,10 +386,10 @@ class DisconnectRequest : public ProtoMessage { protected: }; -class DisconnectResponse : public ProtoMessage { +class DisconnectResponse : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 6; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 6; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "disconnect_response"; } #endif @@ -408,10 +399,10 @@ class DisconnectResponse : public ProtoMessage { protected: }; -class PingRequest : public ProtoMessage { +class PingRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 7; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 7; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "ping_request"; } #endif @@ -421,10 +412,10 @@ class PingRequest : public ProtoMessage { protected: }; -class PingResponse : public ProtoMessage { +class PingResponse : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 8; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 8; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "ping_response"; } #endif @@ -434,10 +425,10 @@ class PingResponse : public ProtoMessage { protected: }; -class DeviceInfoRequest : public ProtoMessage { +class DeviceInfoRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 9; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 9; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_request"; } #endif @@ -447,6 +438,7 @@ class DeviceInfoRequest : public ProtoMessage { protected: }; +#ifdef USE_AREAS class AreaInfo : public ProtoMessage { public: uint32_t area_id{0}; @@ -458,9 +450,9 @@ 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; }; +#endif +#ifdef USE_DEVICES class DeviceInfo : public ProtoMessage { public: uint32_t device_id{0}; @@ -473,38 +465,61 @@ 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; }; +#endif class DeviceInfoResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 10; - static constexpr uint16_t ESTIMATED_SIZE = 219; + static constexpr uint8_t MESSAGE_TYPE = 10; + static constexpr uint8_t ESTIMATED_SIZE = 211; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif +#ifdef USE_API_PASSWORD bool uses_password{false}; +#endif std::string name{}; std::string mac_address{}; 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}; - 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{}; - uint32_t legacy_voice_assistant_version{0}; +#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 @@ -512,13 +527,11 @@ class DeviceInfoResponse : 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 ListEntitiesRequest : public ProtoMessage { +class ListEntitiesRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 11; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 11; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_request"; } #endif @@ -530,8 +543,8 @@ class ListEntitiesRequest : public ProtoMessage { }; class ListEntitiesDoneResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 19; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 19; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_done_response"; } #endif @@ -541,10 +554,10 @@ class ListEntitiesDoneResponse : public ProtoMessage { protected: }; -class SubscribeStatesRequest : public ProtoMessage { +class SubscribeStatesRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 20; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 20; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_states_request"; } #endif @@ -557,8 +570,8 @@ class SubscribeStatesRequest : public ProtoMessage { #ifdef USE_BINARY_SENSOR class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 12; - static constexpr uint16_t ESTIMATED_SIZE = 60; + static constexpr uint8_t MESSAGE_TYPE = 12; + 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 @@ -571,14 +584,11 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BinarySensorStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 21; - static constexpr uint16_t ESTIMATED_SIZE = 13; + static constexpr uint8_t MESSAGE_TYPE = 21; + static constexpr uint8_t ESTIMATED_SIZE = 13; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "binary_sensor_state_response"; } #endif @@ -591,15 +601,13 @@ class BinarySensorStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif #ifdef USE_COVER class ListEntitiesCoverResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 13; - static constexpr uint16_t ESTIMATED_SIZE = 66; + static constexpr uint8_t MESSAGE_TYPE = 13; + static constexpr uint8_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_cover_response"; } #endif @@ -615,18 +623,14 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class CoverStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 22; - static constexpr uint16_t ESTIMATED_SIZE = 23; + static constexpr uint8_t MESSAGE_TYPE = 22; + static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "cover_state_response"; } #endif - enums::LegacyCoverState legacy_state{}; float position{0.0f}; float tilt{0.0f}; enums::CoverOperation current_operation{}; @@ -637,26 +641,19 @@ class CoverStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class CoverCommandRequest : public ProtoMessage { +class CoverCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 30; - static constexpr uint16_t ESTIMATED_SIZE = 25; + static constexpr uint8_t MESSAGE_TYPE = 30; + static constexpr uint8_t ESTIMATED_SIZE = 25; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "cover_command_request"; } #endif - uint32_t key{0}; - bool has_legacy_command{false}; - enums::LegacyCoverCommand legacy_command{}; bool has_position{false}; float position{0.0f}; bool has_tilt{false}; float tilt{0.0f}; bool stop{false}; - 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 @@ -669,8 +666,8 @@ class CoverCommandRequest : public ProtoMessage { #ifdef USE_FAN class ListEntitiesFanResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 14; - static constexpr uint16_t ESTIMATED_SIZE = 77; + static constexpr uint8_t MESSAGE_TYPE = 14; + static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_fan_response"; } #endif @@ -686,20 +683,16 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class FanStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 23; - static constexpr uint16_t ESTIMATED_SIZE = 30; + static constexpr uint8_t MESSAGE_TYPE = 23; + static constexpr uint8_t ESTIMATED_SIZE = 28; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "fan_state_response"; } #endif bool state{false}; bool oscillating{false}; - enums::FanSpeed speed{}; enums::FanDirection direction{}; int32_t speed_level{0}; std::string preset_mode{}; @@ -710,22 +703,16 @@ class FanStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class FanCommandRequest : public ProtoMessage { +class FanCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 31; - static constexpr uint16_t ESTIMATED_SIZE = 38; + static constexpr uint8_t MESSAGE_TYPE = 31; + static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "fan_command_request"; } #endif - uint32_t key{0}; bool has_state{false}; bool state{false}; - bool has_speed{false}; - enums::FanSpeed speed{}; bool has_oscillating{false}; bool oscillating{false}; bool has_direction{false}; @@ -734,8 +721,6 @@ class FanCommandRequest : public ProtoMessage { int32_t speed_level{0}; bool has_preset_mode{false}; std::string preset_mode{}; - 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 @@ -749,16 +734,12 @@ class FanCommandRequest : public ProtoMessage { #ifdef USE_LIGHT class ListEntitiesLightResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 15; - static constexpr uint16_t ESTIMATED_SIZE = 90; + static constexpr uint8_t MESSAGE_TYPE = 15; + static constexpr uint8_t ESTIMATED_SIZE = 73; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_light_response"; } #endif std::vector supported_color_modes{}; - bool legacy_supports_brightness{false}; - bool legacy_supports_rgb{false}; - bool legacy_supports_white_value{false}; - bool legacy_supports_color_temperature{false}; float min_mireds{0.0f}; float max_mireds{0.0f}; std::vector effects{}; @@ -769,14 +750,11 @@ class ListEntitiesLightResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class LightStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 24; - static constexpr uint16_t ESTIMATED_SIZE = 67; + static constexpr uint8_t MESSAGE_TYPE = 24; + static constexpr uint8_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "light_state_response"; } #endif @@ -799,18 +777,14 @@ class LightStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class LightCommandRequest : public ProtoMessage { +class LightCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 32; - static constexpr uint16_t ESTIMATED_SIZE = 107; + static constexpr uint8_t MESSAGE_TYPE = 32; + static constexpr uint8_t ESTIMATED_SIZE = 112; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "light_command_request"; } #endif - uint32_t key{0}; bool has_state{false}; bool state{false}; bool has_brightness{false}; @@ -837,8 +811,6 @@ class LightCommandRequest : public ProtoMessage { uint32_t flash_length{0}; bool has_effect{false}; std::string effect{}; - 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 @@ -852,8 +824,8 @@ class LightCommandRequest : public ProtoMessage { #ifdef USE_SENSOR class ListEntitiesSensorResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 16; - static constexpr uint16_t ESTIMATED_SIZE = 77; + static constexpr uint8_t MESSAGE_TYPE = 16; + static constexpr uint8_t ESTIMATED_SIZE = 66; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_sensor_response"; } #endif @@ -862,7 +834,6 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage { bool force_update{false}; std::string device_class{}; enums::SensorStateClass state_class{}; - enums::SensorLastResetType legacy_last_reset_type{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -870,14 +841,11 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class SensorStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 25; - static constexpr uint16_t ESTIMATED_SIZE = 16; + static constexpr uint8_t MESSAGE_TYPE = 25; + static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "sensor_state_response"; } #endif @@ -890,15 +858,13 @@ class SensorStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif #ifdef USE_SWITCH class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 17; - static constexpr uint16_t ESTIMATED_SIZE = 60; + static constexpr uint8_t MESSAGE_TYPE = 17; + static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_switch_response"; } #endif @@ -911,14 +877,11 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class SwitchStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 26; - static constexpr uint16_t ESTIMATED_SIZE = 11; + static constexpr uint8_t MESSAGE_TYPE = 26; + static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "switch_state_response"; } #endif @@ -930,20 +893,15 @@ class SwitchStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SwitchCommandRequest : public ProtoMessage { +class SwitchCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 33; - static constexpr uint16_t ESTIMATED_SIZE = 7; + static constexpr uint8_t MESSAGE_TYPE = 33; + static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "switch_command_request"; } #endif - uint32_t key{0}; bool state{false}; - 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 @@ -956,8 +914,8 @@ class SwitchCommandRequest : public ProtoMessage { #ifdef USE_TEXT_SENSOR class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 18; - static constexpr uint16_t ESTIMATED_SIZE = 58; + static constexpr uint8_t MESSAGE_TYPE = 18; + 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 @@ -969,14 +927,11 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class TextSensorStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 27; - static constexpr uint16_t ESTIMATED_SIZE = 20; + static constexpr uint8_t MESSAGE_TYPE = 27; + static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "text_sensor_state_response"; } #endif @@ -989,22 +944,17 @@ class TextSensorStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif -class SubscribeLogsRequest : public ProtoMessage { +class SubscribeLogsRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 28; - static constexpr uint16_t ESTIMATED_SIZE = 4; + static constexpr uint8_t MESSAGE_TYPE = 28; + static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_logs_request"; } #endif enums::LogLevel level{}; bool dump_config{false}; - 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 @@ -1014,8 +964,8 @@ class SubscribeLogsRequest : public ProtoMessage { }; class SubscribeLogsResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 29; - static constexpr uint16_t ESTIMATED_SIZE = 13; + static constexpr uint8_t MESSAGE_TYPE = 29; + static constexpr uint8_t ESTIMATED_SIZE = 13; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_logs_response"; } #endif @@ -1029,20 +979,16 @@ class SubscribeLogsResponse : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #ifdef USE_API_NOISE -class NoiseEncryptionSetKeyRequest : public ProtoMessage { +class NoiseEncryptionSetKeyRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 124; - static constexpr uint16_t ESTIMATED_SIZE = 9; + static constexpr uint8_t MESSAGE_TYPE = 124; + static constexpr uint8_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "noise_encryption_set_key_request"; } #endif std::string key{}; - 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 @@ -1052,8 +998,8 @@ class NoiseEncryptionSetKeyRequest : public ProtoMessage { }; class NoiseEncryptionSetKeyResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 125; - static constexpr uint16_t ESTIMATED_SIZE = 2; + static constexpr uint8_t MESSAGE_TYPE = 125; + static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "noise_encryption_set_key_response"; } #endif @@ -1065,13 +1011,12 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif -class SubscribeHomeassistantServicesRequest : public ProtoMessage { +class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 34; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 34; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_homeassistant_services_request"; } #endif @@ -1092,12 +1037,11 @@ class HomeassistantServiceMap : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; class HomeassistantServiceResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 35; - static constexpr uint16_t ESTIMATED_SIZE = 113; + static constexpr uint8_t MESSAGE_TYPE = 35; + static constexpr uint8_t ESTIMATED_SIZE = 113; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "homeassistant_service_response"; } #endif @@ -1113,13 +1057,11 @@ class HomeassistantServiceResponse : 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 SubscribeHomeAssistantStatesRequest : public ProtoMessage { +class SubscribeHomeAssistantStatesRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 38; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 38; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_home_assistant_states_request"; } #endif @@ -1131,8 +1073,8 @@ class SubscribeHomeAssistantStatesRequest : public ProtoMessage { }; class SubscribeHomeAssistantStateResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 39; - static constexpr uint16_t ESTIMATED_SIZE = 20; + static constexpr uint8_t MESSAGE_TYPE = 39; + static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_home_assistant_state_response"; } #endif @@ -1146,21 +1088,17 @@ class SubscribeHomeAssistantStateResponse : 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 HomeAssistantStateResponse : public ProtoMessage { +class HomeAssistantStateResponse : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 40; - static constexpr uint16_t ESTIMATED_SIZE = 27; + static constexpr uint8_t MESSAGE_TYPE = 40; + static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "home_assistant_state_response"; } #endif std::string entity_id{}; std::string state{}; std::string attribute{}; - 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 @@ -1168,10 +1106,10 @@ 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 uint16_t MESSAGE_TYPE = 36; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 36; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "get_time_request"; } #endif @@ -1181,10 +1119,10 @@ class GetTimeRequest : public ProtoMessage { protected: }; -class GetTimeResponse : public ProtoMessage { +class GetTimeResponse : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 37; - static constexpr uint16_t ESTIMATED_SIZE = 5; + static constexpr uint8_t MESSAGE_TYPE = 37; + static constexpr uint8_t ESTIMATED_SIZE = 5; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "get_time_response"; } #endif @@ -1198,6 +1136,7 @@ class GetTimeResponse : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; +#ifdef USE_API_SERVICES class ListEntitiesServicesArgument : public ProtoMessage { public: std::string name{}; @@ -1209,13 +1148,11 @@ 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: - static constexpr uint16_t MESSAGE_TYPE = 41; - static constexpr uint16_t ESTIMATED_SIZE = 48; + static constexpr uint8_t MESSAGE_TYPE = 41; + static constexpr uint8_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_services_response"; } #endif @@ -1229,10 +1166,8 @@ class ListEntitiesServicesResponse : public ProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class ExecuteServiceArgument : public ProtoMessage { +class ExecuteServiceArgument : public ProtoDecodableMessage { public: bool bool_{false}; int32_t legacy_int{0}; @@ -1243,8 +1178,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 @@ -1254,17 +1187,15 @@ 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 uint16_t MESSAGE_TYPE = 42; - static constexpr uint16_t ESTIMATED_SIZE = 39; + static constexpr uint8_t MESSAGE_TYPE = 42; + static constexpr uint8_t ESTIMATED_SIZE = 39; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "execute_service_request"; } #endif uint32_t key{0}; std::vector args{}; - 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 @@ -1273,11 +1204,12 @@ class ExecuteServiceRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; +#endif #ifdef USE_CAMERA class ListEntitiesCameraResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 43; - static constexpr uint16_t ESTIMATED_SIZE = 49; + static constexpr uint8_t MESSAGE_TYPE = 43; + static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_camera_response"; } #endif @@ -1288,18 +1220,14 @@ class ListEntitiesCameraResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class CameraImageResponse : public ProtoMessage { +class CameraImageResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 44; - static constexpr uint16_t ESTIMATED_SIZE = 16; + static constexpr uint8_t MESSAGE_TYPE = 44; + static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "camera_image_response"; } #endif - uint32_t key{0}; std::string data{}; bool done{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1309,21 +1237,16 @@ class CameraImageResponse : public ProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class CameraImageRequest : public ProtoMessage { +class CameraImageRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 45; - static constexpr uint16_t ESTIMATED_SIZE = 4; + static constexpr uint8_t MESSAGE_TYPE = 45; + static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "camera_image_request"; } #endif bool single{false}; bool stream{false}; - 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 @@ -1335,8 +1258,8 @@ class CameraImageRequest : public ProtoMessage { #ifdef USE_CLIMATE class ListEntitiesClimateResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 46; - static constexpr uint16_t ESTIMATED_SIZE = 156; + static constexpr uint8_t MESSAGE_TYPE = 46; + static constexpr uint8_t ESTIMATED_SIZE = 145; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_climate_response"; } #endif @@ -1346,7 +1269,6 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { float visual_min_temperature{0.0f}; float visual_max_temperature{0.0f}; float visual_target_temperature_step{0.0f}; - bool legacy_supports_away{false}; bool supports_action{false}; std::vector supported_fan_modes{}; std::vector supported_swing_modes{}; @@ -1365,14 +1287,11 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class ClimateStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 47; - static constexpr uint16_t ESTIMATED_SIZE = 70; + static constexpr uint8_t MESSAGE_TYPE = 47; + static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "climate_state_response"; } #endif @@ -1381,7 +1300,6 @@ class ClimateStateResponse : public StateResponseProtoMessage { float target_temperature{0.0f}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - bool unused_legacy_away{false}; enums::ClimateAction action{}; enums::ClimateFanMode fan_mode{}; enums::ClimateSwingMode swing_mode{}; @@ -1397,18 +1315,14 @@ class ClimateStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ClimateCommandRequest : public ProtoMessage { +class ClimateCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 48; - static constexpr uint16_t ESTIMATED_SIZE = 83; + static constexpr uint8_t MESSAGE_TYPE = 48; + static constexpr uint8_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "climate_command_request"; } #endif - uint32_t key{0}; bool has_mode{false}; enums::ClimateMode mode{}; bool has_target_temperature{false}; @@ -1417,8 +1331,6 @@ class ClimateCommandRequest : public ProtoMessage { float target_temperature_low{0.0f}; bool has_target_temperature_high{false}; float target_temperature_high{0.0f}; - bool unused_has_legacy_away{false}; - bool unused_legacy_away{false}; bool has_fan_mode{false}; enums::ClimateFanMode fan_mode{}; bool has_swing_mode{false}; @@ -1431,8 +1343,6 @@ class ClimateCommandRequest : public ProtoMessage { std::string custom_preset{}; bool has_target_humidity{false}; float target_humidity{0.0f}; - 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 @@ -1446,8 +1356,8 @@ class ClimateCommandRequest : public ProtoMessage { #ifdef USE_NUMBER class ListEntitiesNumberResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 49; - static constexpr uint16_t ESTIMATED_SIZE = 84; + static constexpr uint8_t MESSAGE_TYPE = 49; + static constexpr uint8_t ESTIMATED_SIZE = 75; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_number_response"; } #endif @@ -1464,14 +1374,11 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class NumberStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 50; - static constexpr uint16_t ESTIMATED_SIZE = 16; + static constexpr uint8_t MESSAGE_TYPE = 50; + static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "number_state_response"; } #endif @@ -1484,33 +1391,29 @@ class NumberStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class NumberCommandRequest : public ProtoMessage { +class NumberCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 51; - static constexpr uint16_t ESTIMATED_SIZE = 10; + static constexpr uint8_t MESSAGE_TYPE = 51; + static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "number_command_request"; } #endif - uint32_t key{0}; float state{0.0f}; - 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 protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif #ifdef USE_SELECT class ListEntitiesSelectResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 52; - static constexpr uint16_t ESTIMATED_SIZE = 67; + static constexpr uint8_t MESSAGE_TYPE = 52; + static constexpr uint8_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_select_response"; } #endif @@ -1522,14 +1425,11 @@ class ListEntitiesSelectResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class SelectStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 53; - static constexpr uint16_t ESTIMATED_SIZE = 20; + static constexpr uint8_t MESSAGE_TYPE = 53; + static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "select_state_response"; } #endif @@ -1542,21 +1442,15 @@ class SelectStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SelectCommandRequest : public ProtoMessage { +class SelectCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 54; - static constexpr uint16_t ESTIMATED_SIZE = 14; + static constexpr uint8_t MESSAGE_TYPE = 54; + static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "select_command_request"; } #endif - uint32_t key{0}; std::string state{}; - 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 @@ -1564,13 +1458,14 @@ class SelectCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif #ifdef USE_SIREN class ListEntitiesSirenResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 55; - static constexpr uint16_t ESTIMATED_SIZE = 71; + static constexpr uint8_t MESSAGE_TYPE = 55; + static constexpr uint8_t ESTIMATED_SIZE = 62; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_siren_response"; } #endif @@ -1584,14 +1479,11 @@ class ListEntitiesSirenResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class SirenStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 56; - static constexpr uint16_t ESTIMATED_SIZE = 11; + static constexpr uint8_t MESSAGE_TYPE = 56; + static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "siren_state_response"; } #endif @@ -1603,17 +1495,14 @@ class SirenStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SirenCommandRequest : public ProtoMessage { +class SirenCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 57; - static constexpr uint16_t ESTIMATED_SIZE = 33; + static constexpr uint8_t MESSAGE_TYPE = 57; + static constexpr uint8_t ESTIMATED_SIZE = 37; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "siren_command_request"; } #endif - uint32_t key{0}; bool has_state{false}; bool state{false}; bool has_tone{false}; @@ -1622,8 +1511,6 @@ class SirenCommandRequest : public ProtoMessage { uint32_t duration{0}; bool has_volume{false}; float volume{0.0f}; - 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 @@ -1637,8 +1524,8 @@ class SirenCommandRequest : public ProtoMessage { #ifdef USE_LOCK class ListEntitiesLockResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 58; - static constexpr uint16_t ESTIMATED_SIZE = 64; + static constexpr uint8_t MESSAGE_TYPE = 58; + static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_lock_response"; } #endif @@ -1653,14 +1540,11 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class LockStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 59; - static constexpr uint16_t ESTIMATED_SIZE = 11; + static constexpr uint8_t MESSAGE_TYPE = 59; + static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "lock_state_response"; } #endif @@ -1672,22 +1556,17 @@ class LockStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class LockCommandRequest : public ProtoMessage { +class LockCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 60; - static constexpr uint16_t ESTIMATED_SIZE = 18; + static constexpr uint8_t MESSAGE_TYPE = 60; + static constexpr uint8_t ESTIMATED_SIZE = 22; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "lock_command_request"; } #endif - uint32_t key{0}; enums::LockCommand command{}; bool has_code{false}; std::string code{}; - 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 @@ -1701,8 +1580,8 @@ class LockCommandRequest : public ProtoMessage { #ifdef USE_BUTTON class ListEntitiesButtonResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 61; - static constexpr uint16_t ESTIMATED_SIZE = 58; + static constexpr uint8_t MESSAGE_TYPE = 61; + static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_button_response"; } #endif @@ -1714,26 +1593,21 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ButtonCommandRequest : public ProtoMessage { +class ButtonCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 62; - static constexpr uint16_t ESTIMATED_SIZE = 5; + static constexpr uint8_t MESSAGE_TYPE = 62; + static constexpr uint8_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "button_command_request"; } #endif - uint32_t key{0}; - 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 protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif #ifdef USE_MEDIA_PLAYER @@ -1751,13 +1625,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 uint16_t MESSAGE_TYPE = 63; - static constexpr uint16_t ESTIMATED_SIZE = 85; + static constexpr uint8_t MESSAGE_TYPE = 63; + 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 @@ -1770,14 +1642,11 @@ class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class MediaPlayerStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 64; - static constexpr uint16_t ESTIMATED_SIZE = 18; + static constexpr uint8_t MESSAGE_TYPE = 64; + static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "media_player_state_response"; } #endif @@ -1791,17 +1660,14 @@ class MediaPlayerStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class MediaPlayerCommandRequest : public ProtoMessage { +class MediaPlayerCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 65; - static constexpr uint16_t ESTIMATED_SIZE = 31; + static constexpr uint8_t MESSAGE_TYPE = 65; + static constexpr uint8_t ESTIMATED_SIZE = 35; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "media_player_command_request"; } #endif - uint32_t key{0}; bool has_command{false}; enums::MediaPlayerCommand command{}; bool has_volume{false}; @@ -1810,8 +1676,6 @@ class MediaPlayerCommandRequest : public ProtoMessage { std::string media_url{}; bool has_announcement{false}; bool announcement{false}; - 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 @@ -1823,16 +1687,14 @@ class MediaPlayerCommandRequest : public ProtoMessage { }; #endif #ifdef USE_BLUETOOTH_PROXY -class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { +class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 66; - static constexpr uint16_t ESTIMATED_SIZE = 4; + static constexpr uint8_t MESSAGE_TYPE = 66; + static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_bluetooth_le_advertisements_request"; } #endif uint32_t flags{0}; - 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 @@ -1840,51 +1702,13 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothServiceData : public ProtoMessage { - public: - std::string uuid{}; - std::vector legacy_data{}; - std::string data{}; - 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 - - 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: - static constexpr uint16_t MESSAGE_TYPE = 67; - static constexpr uint16_t ESTIMATED_SIZE = 107; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_le_advertisement_response"; } -#endif - uint64_t address{0}; - std::string name{}; - int32_t rssi{0}; - std::vector service_uuids{}; - std::vector service_data{}; - std::vector manufacturer_data{}; - uint32_t address_type{0}; - 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 - - protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; -}; class BluetoothLERawAdvertisement : public ProtoMessage { public: 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 @@ -1892,13 +1716,11 @@ 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: - static constexpr uint16_t MESSAGE_TYPE = 93; - static constexpr uint16_t ESTIMATED_SIZE = 34; + static constexpr uint8_t MESSAGE_TYPE = 93; + static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } #endif @@ -1910,12 +1732,11 @@ class BluetoothLERawAdvertisementsResponse : public ProtoMessage { #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class BluetoothDeviceRequest : public ProtoMessage { +class BluetoothDeviceRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 68; - static constexpr uint16_t ESTIMATED_SIZE = 12; + static constexpr uint8_t MESSAGE_TYPE = 68; + static constexpr uint8_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_device_request"; } #endif @@ -1923,8 +1744,6 @@ class BluetoothDeviceRequest : public ProtoMessage { enums::BluetoothDeviceRequestType request_type{}; bool has_address_type{false}; uint32_t address_type{0}; - 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 @@ -1934,8 +1753,8 @@ class BluetoothDeviceRequest : public ProtoMessage { }; class BluetoothDeviceConnectionResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 69; - static constexpr uint16_t ESTIMATED_SIZE = 14; + static constexpr uint8_t MESSAGE_TYPE = 69; + static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_device_connection_response"; } #endif @@ -1950,18 +1769,15 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTGetServicesRequest : public ProtoMessage { +class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 70; - static constexpr uint16_t ESTIMATED_SIZE = 4; + static constexpr uint8_t MESSAGE_TYPE = 70; + static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_get_services_request"; } #endif uint64_t address{0}; - 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 @@ -1980,7 +1796,6 @@ class BluetoothGATTDescriptor : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothGATTCharacteristic : public ProtoMessage { public: @@ -1995,8 +1810,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: @@ -2010,13 +1823,11 @@ 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: - static constexpr uint16_t MESSAGE_TYPE = 71; - static constexpr uint16_t ESTIMATED_SIZE = 38; + static constexpr uint8_t MESSAGE_TYPE = 71; + static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } #endif @@ -2029,13 +1840,11 @@ class BluetoothGATTGetServicesResponse : 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 BluetoothGATTGetServicesDoneResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 72; - static constexpr uint16_t ESTIMATED_SIZE = 4; + static constexpr uint8_t MESSAGE_TYPE = 72; + static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } #endif @@ -2047,19 +1856,16 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTReadRequest : public ProtoMessage { +class BluetoothGATTReadRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 73; - static constexpr uint16_t ESTIMATED_SIZE = 8; + static constexpr uint8_t MESSAGE_TYPE = 73; + static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_read_request"; } #endif uint64_t address{0}; uint32_t handle{0}; - 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 @@ -2069,8 +1875,8 @@ class BluetoothGATTReadRequest : public ProtoMessage { }; class BluetoothGATTReadResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 74; - static constexpr uint16_t ESTIMATED_SIZE = 17; + static constexpr uint8_t MESSAGE_TYPE = 74; + static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_read_response"; } #endif @@ -2084,13 +1890,11 @@ class BluetoothGATTReadResponse : 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 BluetoothGATTWriteRequest : public ProtoMessage { +class BluetoothGATTWriteRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 75; - static constexpr uint16_t ESTIMATED_SIZE = 19; + static constexpr uint8_t MESSAGE_TYPE = 75; + static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_write_request"; } #endif @@ -2098,8 +1902,6 @@ class BluetoothGATTWriteRequest : public ProtoMessage { uint32_t handle{0}; bool response{false}; std::string data{}; - 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 @@ -2108,17 +1910,15 @@ 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 uint16_t MESSAGE_TYPE = 76; - static constexpr uint16_t ESTIMATED_SIZE = 8; + static constexpr uint8_t MESSAGE_TYPE = 76; + static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_read_descriptor_request"; } #endif uint64_t address{0}; uint32_t handle{0}; - 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 @@ -2126,18 +1926,16 @@ 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 uint16_t MESSAGE_TYPE = 77; - static constexpr uint16_t ESTIMATED_SIZE = 17; + static constexpr uint8_t MESSAGE_TYPE = 77; + static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; } #endif uint64_t address{0}; uint32_t handle{0}; std::string data{}; - 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 @@ -2146,18 +1944,16 @@ 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 uint16_t MESSAGE_TYPE = 78; - static constexpr uint16_t ESTIMATED_SIZE = 10; + static constexpr uint8_t MESSAGE_TYPE = 78; + static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_notify_request"; } #endif uint64_t address{0}; uint32_t handle{0}; bool enable{false}; - 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 @@ -2167,8 +1963,8 @@ class BluetoothGATTNotifyRequest : public ProtoMessage { }; class BluetoothGATTNotifyDataResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 79; - static constexpr uint16_t ESTIMATED_SIZE = 17; + static constexpr uint8_t MESSAGE_TYPE = 79; + static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_notify_data_response"; } #endif @@ -2182,13 +1978,11 @@ class BluetoothGATTNotifyDataResponse : 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 SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { +class SubscribeBluetoothConnectionsFreeRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 80; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 80; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_bluetooth_connections_free_request"; } #endif @@ -2200,8 +1994,8 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { }; class BluetoothConnectionsFreeResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 81; - static constexpr uint16_t ESTIMATED_SIZE = 16; + static constexpr uint8_t MESSAGE_TYPE = 81; + static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_connections_free_response"; } #endif @@ -2215,12 +2009,11 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothGATTErrorResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 82; - static constexpr uint16_t ESTIMATED_SIZE = 12; + static constexpr uint8_t MESSAGE_TYPE = 82; + static constexpr uint8_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_error_response"; } #endif @@ -2234,12 +2027,11 @@ class BluetoothGATTErrorResponse : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothGATTWriteResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 83; - static constexpr uint16_t ESTIMATED_SIZE = 8; + static constexpr uint8_t MESSAGE_TYPE = 83; + static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_write_response"; } #endif @@ -2252,12 +2044,11 @@ class BluetoothGATTWriteResponse : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothGATTNotifyResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 84; - static constexpr uint16_t ESTIMATED_SIZE = 8; + static constexpr uint8_t MESSAGE_TYPE = 84; + static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_notify_response"; } #endif @@ -2270,12 +2061,11 @@ class BluetoothGATTNotifyResponse : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothDevicePairingResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 85; - static constexpr uint16_t ESTIMATED_SIZE = 10; + static constexpr uint8_t MESSAGE_TYPE = 85; + static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_device_pairing_response"; } #endif @@ -2289,12 +2079,11 @@ class BluetoothDevicePairingResponse : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothDeviceUnpairingResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 86; - static constexpr uint16_t ESTIMATED_SIZE = 10; + static constexpr uint8_t MESSAGE_TYPE = 86; + static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_device_unpairing_response"; } #endif @@ -2308,12 +2097,11 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { +class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 87; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 87; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "unsubscribe_bluetooth_le_advertisements_request"; } #endif @@ -2325,8 +2113,8 @@ class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { }; class BluetoothDeviceClearCacheResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 88; - static constexpr uint16_t ESTIMATED_SIZE = 10; + static constexpr uint8_t MESSAGE_TYPE = 88; + static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_device_clear_cache_response"; } #endif @@ -2340,12 +2128,11 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothScannerStateResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 126; - static constexpr uint16_t ESTIMATED_SIZE = 4; + static constexpr uint8_t MESSAGE_TYPE = 126; + static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_scanner_state_response"; } #endif @@ -2358,18 +2145,15 @@ class BluetoothScannerStateResponse : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothScannerSetModeRequest : public ProtoMessage { +class BluetoothScannerSetModeRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 127; - static constexpr uint16_t ESTIMATED_SIZE = 2; + static constexpr uint8_t MESSAGE_TYPE = 127; + static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_scanner_set_mode_request"; } #endif enums::BluetoothScannerMode mode{}; - 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 @@ -2379,17 +2163,15 @@ class BluetoothScannerSetModeRequest : public ProtoMessage { }; #endif #ifdef USE_VOICE_ASSISTANT -class SubscribeVoiceAssistantRequest : public ProtoMessage { +class SubscribeVoiceAssistantRequest : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 89; - static constexpr uint16_t ESTIMATED_SIZE = 6; + static constexpr uint8_t MESSAGE_TYPE = 89; + static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_voice_assistant_request"; } #endif bool subscribe{false}; uint32_t flags{0}; - 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 @@ -2409,13 +2191,11 @@ 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: - static constexpr uint16_t MESSAGE_TYPE = 90; - static constexpr uint16_t ESTIMATED_SIZE = 41; + static constexpr uint8_t MESSAGE_TYPE = 90; + static constexpr uint8_t ESTIMATED_SIZE = 41; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_request"; } #endif @@ -2431,20 +2211,16 @@ class VoiceAssistantRequest : 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 VoiceAssistantResponse : public ProtoMessage { +class VoiceAssistantResponse : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 91; - static constexpr uint16_t ESTIMATED_SIZE = 6; + static constexpr uint8_t MESSAGE_TYPE = 91; + static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_response"; } #endif uint32_t port{0}; bool error{false}; - 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 @@ -2452,12 +2228,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 @@ -2465,17 +2239,15 @@ 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 uint16_t MESSAGE_TYPE = 92; - static constexpr uint16_t ESTIMATED_SIZE = 36; + static constexpr uint8_t MESSAGE_TYPE = 92; + static constexpr uint8_t ESTIMATED_SIZE = 36; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_event_response"; } #endif enums::VoiceAssistantEvent event_type{}; std::vector data{}; - 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 @@ -2484,10 +2256,10 @@ 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 uint16_t MESSAGE_TYPE = 106; - static constexpr uint16_t ESTIMATED_SIZE = 11; + static constexpr uint8_t MESSAGE_TYPE = 106; + static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_audio"; } #endif @@ -2503,10 +2275,10 @@ 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 uint16_t MESSAGE_TYPE = 115; - static constexpr uint16_t ESTIMATED_SIZE = 30; + static constexpr uint8_t MESSAGE_TYPE = 115; + static constexpr uint8_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_timer_event_response"; } #endif @@ -2516,8 +2288,6 @@ class VoiceAssistantTimerEventResponse : public ProtoMessage { uint32_t total_seconds{0}; uint32_t seconds_left{0}; bool is_active{false}; - 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 @@ -2526,10 +2296,10 @@ 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 uint16_t MESSAGE_TYPE = 119; - static constexpr uint16_t ESTIMATED_SIZE = 29; + static constexpr uint8_t MESSAGE_TYPE = 119; + static constexpr uint8_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_announce_request"; } #endif @@ -2537,8 +2307,6 @@ class VoiceAssistantAnnounceRequest : public ProtoMessage { std::string text{}; std::string preannounce_media_id{}; bool start_conversation{false}; - 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 @@ -2549,8 +2317,8 @@ class VoiceAssistantAnnounceRequest : public ProtoMessage { }; class VoiceAssistantAnnounceFinished : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 120; - static constexpr uint16_t ESTIMATED_SIZE = 2; + static constexpr uint8_t MESSAGE_TYPE = 120; + static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_announce_finished"; } #endif @@ -2562,7 +2330,6 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class VoiceAssistantWakeWord : public ProtoMessage { public: @@ -2576,12 +2343,11 @@ 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 uint16_t MESSAGE_TYPE = 121; - static constexpr uint16_t ESTIMATED_SIZE = 0; + static constexpr uint8_t MESSAGE_TYPE = 121; + static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_configuration_request"; } #endif @@ -2593,8 +2359,8 @@ class VoiceAssistantConfigurationRequest : public ProtoMessage { }; class VoiceAssistantConfigurationResponse : public ProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 122; - static constexpr uint16_t ESTIMATED_SIZE = 56; + static constexpr uint8_t MESSAGE_TYPE = 122; + static constexpr uint8_t ESTIMATED_SIZE = 56; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_configuration_response"; } #endif @@ -2608,19 +2374,15 @@ class VoiceAssistantConfigurationResponse : 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 VoiceAssistantSetConfiguration : public ProtoMessage { +class VoiceAssistantSetConfiguration : public ProtoDecodableMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 123; - static constexpr uint16_t ESTIMATED_SIZE = 18; + static constexpr uint8_t MESSAGE_TYPE = 123; + static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_set_configuration"; } #endif std::vector active_wake_words{}; - 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 @@ -2632,8 +2394,8 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { #ifdef USE_ALARM_CONTROL_PANEL class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 94; - static constexpr uint16_t ESTIMATED_SIZE = 57; + static constexpr uint8_t MESSAGE_TYPE = 94; + 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 @@ -2647,14 +2409,11 @@ class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class AlarmControlPanelStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 95; - static constexpr uint16_t ESTIMATED_SIZE = 11; + static constexpr uint8_t MESSAGE_TYPE = 95; + static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "alarm_control_panel_state_response"; } #endif @@ -2666,21 +2425,16 @@ class AlarmControlPanelStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class AlarmControlPanelCommandRequest : public ProtoMessage { +class AlarmControlPanelCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 96; - static constexpr uint16_t ESTIMATED_SIZE = 16; + static constexpr uint8_t MESSAGE_TYPE = 96; + static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "alarm_control_panel_command_request"; } #endif - uint32_t key{0}; enums::AlarmControlPanelStateCommand command{}; std::string code{}; - 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 @@ -2694,8 +2448,8 @@ class AlarmControlPanelCommandRequest : public ProtoMessage { #ifdef USE_TEXT class ListEntitiesTextResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 97; - static constexpr uint16_t ESTIMATED_SIZE = 68; + static constexpr uint8_t MESSAGE_TYPE = 97; + static constexpr uint8_t ESTIMATED_SIZE = 59; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_text_response"; } #endif @@ -2710,14 +2464,11 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class TextStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 98; - static constexpr uint16_t ESTIMATED_SIZE = 20; + static constexpr uint8_t MESSAGE_TYPE = 98; + static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "text_state_response"; } #endif @@ -2730,21 +2481,15 @@ class TextStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class TextCommandRequest : public ProtoMessage { +class TextCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 99; - static constexpr uint16_t ESTIMATED_SIZE = 14; + static constexpr uint8_t MESSAGE_TYPE = 99; + static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "text_command_request"; } #endif - uint32_t key{0}; std::string state{}; - 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 @@ -2752,13 +2497,14 @@ class TextCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif #ifdef USE_DATETIME_DATE class ListEntitiesDateResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 100; - static constexpr uint16_t ESTIMATED_SIZE = 49; + static constexpr uint8_t MESSAGE_TYPE = 100; + static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_response"; } #endif @@ -2769,14 +2515,11 @@ class ListEntitiesDateResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class DateStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 101; - static constexpr uint16_t ESTIMATED_SIZE = 23; + static constexpr uint8_t MESSAGE_TYPE = 101; + static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "date_state_response"; } #endif @@ -2791,22 +2534,17 @@ class DateStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class DateCommandRequest : public ProtoMessage { +class DateCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 102; - static constexpr uint16_t ESTIMATED_SIZE = 17; + static constexpr uint8_t MESSAGE_TYPE = 102; + static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "date_command_request"; } #endif - uint32_t key{0}; uint32_t year{0}; uint32_t month{0}; uint32_t day{0}; - 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 @@ -2819,8 +2557,8 @@ class DateCommandRequest : public ProtoMessage { #ifdef USE_DATETIME_TIME class ListEntitiesTimeResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 103; - static constexpr uint16_t ESTIMATED_SIZE = 49; + static constexpr uint8_t MESSAGE_TYPE = 103; + static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_time_response"; } #endif @@ -2831,14 +2569,11 @@ class ListEntitiesTimeResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class TimeStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 104; - static constexpr uint16_t ESTIMATED_SIZE = 23; + static constexpr uint8_t MESSAGE_TYPE = 104; + static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "time_state_response"; } #endif @@ -2853,22 +2588,17 @@ class TimeStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class TimeCommandRequest : public ProtoMessage { +class TimeCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 105; - static constexpr uint16_t ESTIMATED_SIZE = 17; + static constexpr uint8_t MESSAGE_TYPE = 105; + static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "time_command_request"; } #endif - uint32_t key{0}; uint32_t hour{0}; uint32_t minute{0}; uint32_t second{0}; - 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 @@ -2881,8 +2611,8 @@ class TimeCommandRequest : public ProtoMessage { #ifdef USE_EVENT class ListEntitiesEventResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 107; - static constexpr uint16_t ESTIMATED_SIZE = 76; + static constexpr uint8_t MESSAGE_TYPE = 107; + static constexpr uint8_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_event_response"; } #endif @@ -2895,14 +2625,11 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class EventResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 108; - static constexpr uint16_t ESTIMATED_SIZE = 18; + static constexpr uint8_t MESSAGE_TYPE = 108; + static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "event_response"; } #endif @@ -2914,16 +2641,13 @@ class EventResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif #ifdef USE_VALVE class ListEntitiesValveResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 109; - static constexpr uint16_t ESTIMATED_SIZE = 64; + static constexpr uint8_t MESSAGE_TYPE = 109; + static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_valve_response"; } #endif @@ -2938,14 +2662,11 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class ValveStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 110; - static constexpr uint16_t ESTIMATED_SIZE = 16; + static constexpr uint8_t MESSAGE_TYPE = 110; + static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "valve_state_response"; } #endif @@ -2958,22 +2679,17 @@ class ValveStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ValveCommandRequest : public ProtoMessage { +class ValveCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 111; - static constexpr uint16_t ESTIMATED_SIZE = 14; + static constexpr uint8_t MESSAGE_TYPE = 111; + static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "valve_command_request"; } #endif - uint32_t key{0}; bool has_position{false}; float position{0.0f}; bool stop{false}; - 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 @@ -2986,8 +2702,8 @@ class ValveCommandRequest : public ProtoMessage { #ifdef USE_DATETIME_DATETIME class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 112; - static constexpr uint16_t ESTIMATED_SIZE = 49; + static constexpr uint8_t MESSAGE_TYPE = 112; + 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 @@ -2998,14 +2714,11 @@ class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class DateTimeStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 113; - static constexpr uint16_t ESTIMATED_SIZE = 16; + static constexpr uint8_t MESSAGE_TYPE = 113; + static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "date_time_state_response"; } #endif @@ -3018,33 +2731,29 @@ class DateTimeStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class DateTimeCommandRequest : public ProtoMessage { +class DateTimeCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 114; - static constexpr uint16_t ESTIMATED_SIZE = 10; + static constexpr uint8_t MESSAGE_TYPE = 114; + static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "date_time_command_request"; } #endif - uint32_t key{0}; uint32_t epoch_seconds{0}; - 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 protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif #ifdef USE_UPDATE class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 116; - static constexpr uint16_t ESTIMATED_SIZE = 58; + static constexpr uint8_t MESSAGE_TYPE = 116; + static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_update_response"; } #endif @@ -3056,14 +2765,11 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class UpdateStateResponse : public StateResponseProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 117; - static constexpr uint16_t ESTIMATED_SIZE = 65; + static constexpr uint8_t MESSAGE_TYPE = 117; + static constexpr uint8_t ESTIMATED_SIZE = 65; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "update_state_response"; } #endif @@ -3083,21 +2789,15 @@ class UpdateStateResponse : public StateResponseProtoMessage { #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class UpdateCommandRequest : public ProtoMessage { +class UpdateCommandRequest : public CommandProtoMessage { public: - static constexpr uint16_t MESSAGE_TYPE = 118; - static constexpr uint16_t ESTIMATED_SIZE = 7; + static constexpr uint8_t MESSAGE_TYPE = 118; + static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "update_command_request"; } #endif - uint32_t key{0}; enums::UpdateCommand command{}; - 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 diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 48ddd42d61..4951c6cebf 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -23,16 +23,6 @@ template<> const char *proto_enum_to_string(enums::Entity } } #ifdef USE_COVER -template<> const char *proto_enum_to_string(enums::LegacyCoverState value) { - switch (value) { - case enums::LEGACY_COVER_STATE_OPEN: - return "LEGACY_COVER_STATE_OPEN"; - case enums::LEGACY_COVER_STATE_CLOSED: - return "LEGACY_COVER_STATE_CLOSED"; - default: - return "UNKNOWN"; - } -} template<> const char *proto_enum_to_string(enums::CoverOperation value) { switch (value) { case enums::COVER_OPERATION_IDLE: @@ -45,32 +35,8 @@ template<> const char *proto_enum_to_string(enums::CoverO return "UNKNOWN"; } } -template<> const char *proto_enum_to_string(enums::LegacyCoverCommand value) { - switch (value) { - case enums::LEGACY_COVER_COMMAND_OPEN: - return "LEGACY_COVER_COMMAND_OPEN"; - case enums::LEGACY_COVER_COMMAND_CLOSE: - return "LEGACY_COVER_COMMAND_CLOSE"; - case enums::LEGACY_COVER_COMMAND_STOP: - return "LEGACY_COVER_COMMAND_STOP"; - default: - return "UNKNOWN"; - } -} #endif #ifdef USE_FAN -template<> const char *proto_enum_to_string(enums::FanSpeed value) { - switch (value) { - case enums::FAN_SPEED_LOW: - return "FAN_SPEED_LOW"; - case enums::FAN_SPEED_MEDIUM: - return "FAN_SPEED_MEDIUM"; - case enums::FAN_SPEED_HIGH: - return "FAN_SPEED_HIGH"; - default: - return "UNKNOWN"; - } -} template<> const char *proto_enum_to_string(enums::FanDirection value) { switch (value) { case enums::FAN_DIRECTION_FORWARD: @@ -127,18 +93,6 @@ template<> const char *proto_enum_to_string(enums::Sens return "UNKNOWN"; } } -template<> const char *proto_enum_to_string(enums::SensorLastResetType value) { - switch (value) { - case enums::LAST_RESET_NONE: - return "LAST_RESET_NONE"; - case enums::LAST_RESET_NEVER: - return "LAST_RESET_NEVER"; - case enums::LAST_RESET_AUTO: - return "LAST_RESET_AUTO"; - default: - return "UNKNOWN"; - } -} #endif template<> const char *proto_enum_to_string(enums::LogLevel value) { switch (value) { @@ -162,6 +116,7 @@ template<> const char *proto_enum_to_string(enums::LogLevel val return "UNKNOWN"; } } +#ifdef USE_API_SERVICES template<> const char *proto_enum_to_string(enums::ServiceArgType value) { switch (value) { case enums::SERVICE_ARG_TYPE_BOOL: @@ -184,6 +139,7 @@ template<> const char *proto_enum_to_string(enums::Servic return "UNKNOWN"; } } +#endif #ifdef USE_CLIMATE template<> const char *proto_enum_to_string(enums::ClimateMode value) { switch (value) { @@ -653,6 +609,7 @@ void DisconnectResponse::dump_to(std::string &out) const { out.append("Disconnec void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); } void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } +#ifdef USE_AREAS void AreaInfo::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("AreaInfo {\n"); @@ -666,6 +623,8 @@ void AreaInfo::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif +#ifdef USE_DEVICES void DeviceInfo::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DeviceInfo {\n"); @@ -684,13 +643,16 @@ void DeviceInfo::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#endif void DeviceInfoResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("DeviceInfoResponse {\n"); +#ifdef USE_API_PASSWORD out.append(" uses_password: "); out.append(YESNO(this->uses_password)); out.append("\n"); +#endif out.append(" name: "); out.append("'").append(this->name).append("'"); out.append("\n"); @@ -711,33 +673,38 @@ 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"); - 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"); @@ -746,43 +713,53 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append("'").append(this->friendly_name).append("'"); out.append("\n"); - out.append(" legacy_voice_assistant_version: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->legacy_voice_assistant_version); - out.append(buffer); - out.append("\n"); - +#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 {}"); } @@ -805,10 +782,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"); @@ -821,18 +794,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 { @@ -851,10 +829,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 @@ -875,10 +856,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"); @@ -899,10 +876,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"); @@ -911,10 +890,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 { @@ -925,10 +907,6 @@ void CoverStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" legacy_state: "); - out.append(proto_enum_to_string(this->legacy_state)); - out.append("\n"); - out.append(" position: "); snprintf(buffer, sizeof(buffer), "%g", this->position); out.append(buffer); @@ -943,10 +921,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 { @@ -957,14 +938,6 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" has_legacy_command: "); - out.append(YESNO(this->has_legacy_command)); - out.append("\n"); - - out.append(" legacy_command: "); - out.append(proto_enum_to_string(this->legacy_command)); - out.append("\n"); - out.append(" has_position: "); out.append(YESNO(this->has_position)); out.append("\n"); @@ -986,6 +959,14 @@ void CoverCommandRequest::dump_to(std::string &out) const { out.append(" stop: "); 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 @@ -1006,10 +987,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"); @@ -1031,10 +1008,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"); @@ -1045,10 +1024,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 { @@ -1067,10 +1049,6 @@ void FanStateResponse::dump_to(std::string &out) const { out.append(YESNO(this->oscillating)); out.append("\n"); - out.append(" speed: "); - out.append(proto_enum_to_string(this->speed)); - out.append("\n"); - out.append(" direction: "); out.append(proto_enum_to_string(this->direction)); out.append("\n"); @@ -1084,10 +1062,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 { @@ -1106,14 +1087,6 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append(YESNO(this->state)); out.append("\n"); - out.append(" has_speed: "); - out.append(YESNO(this->has_speed)); - out.append("\n"); - - out.append(" speed: "); - out.append(proto_enum_to_string(this->speed)); - out.append("\n"); - out.append(" has_oscillating: "); out.append(YESNO(this->has_oscillating)); out.append("\n"); @@ -1146,6 +1119,14 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append(" preset_mode: "); 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 @@ -1166,32 +1147,12 @@ 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)); out.append("\n"); } - out.append(" legacy_supports_brightness: "); - out.append(YESNO(this->legacy_supports_brightness)); - out.append("\n"); - - out.append(" legacy_supports_rgb: "); - out.append(YESNO(this->legacy_supports_rgb)); - out.append("\n"); - - out.append(" legacy_supports_white_value: "); - out.append(YESNO(this->legacy_supports_white_value)); - out.append("\n"); - - out.append(" legacy_supports_color_temperature: "); - out.append(YESNO(this->legacy_supports_color_temperature)); - out.append("\n"); - out.append(" min_mireds: "); snprintf(buffer, sizeof(buffer), "%g", this->min_mireds); out.append(buffer); @@ -1212,18 +1173,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 { @@ -1291,10 +1257,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 { @@ -1419,6 +1388,14 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append(" effect: "); 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 @@ -1439,14 +1416,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"); @@ -1468,10 +1443,6 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append(proto_enum_to_string(this->state_class)); out.append("\n"); - out.append(" legacy_last_reset_type: "); - out.append(proto_enum_to_string(this->legacy_last_reset_type)); - out.append("\n"); - out.append(" disabled_by_default: "); out.append(YESNO(this->disabled_by_default)); out.append("\n"); @@ -1480,10 +1451,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 { @@ -1503,10 +1477,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 @@ -1527,14 +1504,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"); @@ -1551,10 +1526,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 { @@ -1569,10 +1547,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 { @@ -1586,6 +1567,14 @@ void SwitchCommandRequest::dump_to(std::string &out) const { out.append(" state: "); 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 @@ -1606,14 +1595,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"); @@ -1626,10 +1613,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 { @@ -1648,10 +1638,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 @@ -1791,6 +1784,7 @@ void GetTimeResponse::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +#ifdef USE_API_SERVICES void ListEntitiesServicesArgument::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesServicesArgument {\n"); @@ -1890,6 +1884,7 @@ void ExecuteServiceRequest::dump_to(std::string &out) const { } out.append("}"); } +#endif #ifdef USE_CAMERA void ListEntitiesCameraResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -1907,26 +1902,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 { @@ -1944,6 +1940,14 @@ void CameraImageResponse::dump_to(std::string &out) const { out.append(" done: "); 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 { @@ -1976,10 +1980,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"); @@ -2009,10 +2009,6 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" legacy_supports_away: "); - out.append(YESNO(this->legacy_supports_away)); - out.append("\n"); - out.append(" supports_action: "); out.append(YESNO(this->supports_action)); out.append("\n"); @@ -2051,10 +2047,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"); @@ -2082,10 +2080,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 { @@ -2120,10 +2121,6 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" unused_legacy_away: "); - out.append(YESNO(this->unused_legacy_away)); - out.append("\n"); - out.append(" action: "); out.append(proto_enum_to_string(this->action)); out.append("\n"); @@ -2158,10 +2155,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 { @@ -2207,14 +2207,6 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" unused_has_legacy_away: "); - out.append(YESNO(this->unused_has_legacy_away)); - out.append("\n"); - - out.append(" unused_legacy_away: "); - out.append(YESNO(this->unused_legacy_away)); - out.append("\n"); - out.append(" has_fan_mode: "); out.append(YESNO(this->has_fan_mode)); out.append("\n"); @@ -2263,6 +2255,14 @@ void ClimateCommandRequest::dump_to(std::string &out) const { snprintf(buffer, sizeof(buffer), "%g", this->target_humidity); 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 @@ -2283,14 +2283,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); @@ -2326,10 +2324,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 { @@ -2349,10 +2350,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 { @@ -2367,6 +2371,14 @@ void NumberCommandRequest::dump_to(std::string &out) const { snprintf(buffer, sizeof(buffer), "%g", this->state); 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 @@ -2387,14 +2399,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("'"); @@ -2409,10 +2419,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 { @@ -2431,10 +2444,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 { @@ -2448,6 +2464,14 @@ void SelectCommandRequest::dump_to(std::string &out) const { out.append(" state: "); 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 @@ -2468,14 +2492,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"); @@ -2498,10 +2520,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 { @@ -2516,10 +2541,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 { @@ -2563,6 +2591,14 @@ void SirenCommandRequest::dump_to(std::string &out) const { snprintf(buffer, sizeof(buffer), "%g", this->volume); 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 @@ -2583,14 +2619,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"); @@ -2615,10 +2649,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 { @@ -2633,10 +2670,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 { @@ -2658,6 +2698,14 @@ void LockCommandRequest::dump_to(std::string &out) const { out.append(" code: "); 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 @@ -2678,14 +2726,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"); @@ -2698,10 +2744,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 { @@ -2711,6 +2760,14 @@ void ButtonCommandRequest::dump_to(std::string &out) const { snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); 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 @@ -2758,14 +2815,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"); @@ -2784,10 +2839,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 { @@ -2811,10 +2869,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 { @@ -2857,6 +2918,14 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { out.append(" announcement: "); 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 @@ -2870,66 +2939,6 @@ void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const out.append("\n"); out.append("}"); } -void BluetoothServiceData::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothServiceData {\n"); - out.append(" uuid: "); - out.append("'").append(this->uuid).append("'"); - out.append("\n"); - - for (const auto &it : this->legacy_data) { - out.append(" legacy_data: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, it); - out.append(buffer); - out.append("\n"); - } - - out.append(" data: "); - out.append(format_hex_pretty(this->data)); - out.append("\n"); - out.append("}"); -} -void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothLEAdvertisementResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append(format_hex_pretty(this->name)); - out.append("\n"); - - out.append(" rssi: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi); - out.append(buffer); - out.append("\n"); - - for (const auto &it : this->service_uuids) { - out.append(" service_uuids: "); - out.append("'").append(it).append("'"); - out.append("\n"); - } - - for (const auto &it : this->service_data) { - out.append(" service_data: "); - it.dump_to(out); - out.append("\n"); - } - - for (const auto &it : this->manufacturer_data) { - out.append(" manufacturer_data: "); - it.dump_to(out); - out.append("\n"); - } - - out.append(" address_type: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); - out.append(buffer); - out.append("\n"); - out.append("}"); -} void BluetoothLERawAdvertisement::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("BluetoothLERawAdvertisement {\n"); @@ -2949,7 +2958,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("}"); } @@ -3614,14 +3623,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"); @@ -3643,10 +3650,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 { @@ -3661,10 +3671,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 { @@ -3682,6 +3695,14 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { out.append(" code: "); 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 @@ -3702,14 +3723,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"); @@ -3736,10 +3755,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 { @@ -3758,10 +3780,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 { @@ -3775,6 +3800,14 @@ void TextCommandRequest::dump_to(std::string &out) const { out.append(" state: "); 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 @@ -3795,14 +3828,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"); @@ -3811,10 +3842,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 { @@ -3844,10 +3878,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 { @@ -3872,6 +3909,14 @@ void DateCommandRequest::dump_to(std::string &out) const { snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day); 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 @@ -3892,14 +3937,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"); @@ -3908,10 +3951,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 { @@ -3941,10 +3987,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 { @@ -3969,6 +4018,14 @@ void TimeCommandRequest::dump_to(std::string &out) const { snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second); 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 @@ -3989,14 +4046,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"); @@ -4015,10 +4070,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 { @@ -4033,10 +4091,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 @@ -4057,14 +4118,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"); @@ -4089,10 +4148,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 { @@ -4112,10 +4174,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 { @@ -4138,6 +4203,14 @@ void ValveCommandRequest::dump_to(std::string &out) const { out.append(" stop: "); 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 @@ -4158,14 +4231,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"); @@ -4174,10 +4245,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 { @@ -4197,10 +4271,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 { @@ -4215,6 +4292,14 @@ void DateTimeCommandRequest::dump_to(std::string &out) const { snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); 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 @@ -4235,14 +4320,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"); @@ -4255,10 +4338,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 { @@ -4306,10 +4392,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 { @@ -4323,6 +4412,14 @@ void UpdateCommandRequest::dump_to(std::string &out) const { out.append(" command: "); 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 92dd90053b..888dc16836 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -195,6 +195,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_home_assistant_state_response(msg); break; } +#ifdef USE_API_SERVICES case 42: { ExecuteServiceRequest msg; msg.decode(msg_data, msg_size); @@ -204,6 +205,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_execute_service_request(msg); break; } +#endif #ifdef USE_CAMERA case 45: { CameraImageRequest msg; @@ -596,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(); } } @@ -655,21 +657,23 @@ 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(); } } } +#ifdef USE_API_SERVICES void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { if (this->check_authenticated_()) { this->execute_service(msg); } } +#endif #ifdef USE_API_NOISE 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(); } } @@ -863,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(); } } @@ -895,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 458f8ec81b..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){}; @@ -69,7 +69,9 @@ class APIServerConnectionBase : public ProtoService { virtual void on_get_time_request(const GetTimeRequest &value){}; virtual void on_get_time_response(const GetTimeResponse &value){}; +#ifdef USE_API_SERVICES virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; +#endif #ifdef USE_CAMERA virtual void on_camera_image_request(const CameraImageRequest &value){}; @@ -216,7 +218,9 @@ class APIServerConnection : public APIServerConnectionBase { virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; +#ifdef USE_API_SERVICES virtual void execute_service(const ExecuteServiceRequest &msg) = 0; +#endif #ifdef USE_API_NOISE virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0; #endif @@ -333,7 +337,9 @@ class APIServerConnection : public APIServerConnectionBase { void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; void on_get_time_request(const GetTimeRequest &msg) override; +#ifdef USE_API_SERVICES void on_execute_service_request(const ExecuteServiceRequest &msg) override; +#endif #ifdef USE_API_NOISE void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; #endif diff --git a/esphome/components/api/api_pb2_size.h b/esphome/components/api/api_pb2_size.h deleted file mode 100644 index f371be13a5..0000000000 --- a/esphome/components/api/api_pb2_size.h +++ /dev/null @@ -1,359 +0,0 @@ -#pragma once - -#include "proto.h" -#include -#include - -namespace esphome { -namespace api { - -class ProtoSize { - public: - /** - * @brief ProtoSize class for Protocol Buffer serialization size calculation - * - * This class provides static methods to calculate the exact byte counts needed - * for encoding various Protocol Buffer field types. All methods are designed to be - * efficient for the common case where many fields have default values. - * - * Implements Protocol Buffer encoding size calculation according to: - * https://protobuf.dev/programming-guides/encoding/ - * - * Key features: - * - Early-return optimization for zero/default values - * - Direct total_size updates to avoid unnecessary additions - * - Specialized handling for different field types according to protobuf spec - * - Templated helpers for repeated fields and messages - */ - - /** - * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint - * - * @param value The uint32_t value to calculate size for - * @return The number of bytes needed to encode the value - */ - static inline uint32_t varint(uint32_t value) { - // Optimized varint size calculation using leading zeros - // Each 7 bits requires one byte in the varint encoding - if (value < 128) - return 1; // 7 bits, common case for small values - - // For larger values, count bytes needed based on the position of the highest bit set - if (value < 16384) { - return 2; // 14 bits - } else if (value < 2097152) { - return 3; // 21 bits - } else if (value < 268435456) { - return 4; // 28 bits - } else { - return 5; // 32 bits (maximum for uint32_t) - } - } - - /** - * @brief Calculates the size in bytes needed to encode a uint64_t value as a varint - * - * @param value The uint64_t value to calculate size for - * @return The number of bytes needed to encode the value - */ - static inline uint32_t varint(uint64_t value) { - // Handle common case of values fitting in uint32_t (vast majority of use cases) - if (value <= UINT32_MAX) { - return varint(static_cast(value)); - } - - // For larger values, determine size based on highest bit position - if (value < (1ULL << 35)) { - return 5; // 35 bits - } else if (value < (1ULL << 42)) { - return 6; // 42 bits - } else if (value < (1ULL << 49)) { - return 7; // 49 bits - } else if (value < (1ULL << 56)) { - return 8; // 56 bits - } else if (value < (1ULL << 63)) { - return 9; // 63 bits - } else { - return 10; // 64 bits (maximum for uint64_t) - } - } - - /** - * @brief Calculates the size in bytes needed to encode an int32_t value as a varint - * - * Special handling is needed for negative values, which are sign-extended to 64 bits - * in Protocol Buffers, resulting in a 10-byte varint. - * - * @param value The int32_t value to calculate size for - * @return The number of bytes needed to encode the value - */ - static inline uint32_t varint(int32_t value) { - // Negative values are sign-extended to 64 bits in protocol buffers, - // which always results in a 10-byte varint for negative int32 - if (value < 0) { - return 10; // Negative int32 is always 10 bytes long - } - // For non-negative values, use the uint32_t implementation - return varint(static_cast(value)); - } - - /** - * @brief Calculates the size in bytes needed to encode an int64_t value as a varint - * - * @param value The int64_t value to calculate size for - * @return The number of bytes needed to encode the value - */ - static inline uint32_t varint(int64_t value) { - // For int64_t, we convert to uint64_t and calculate the size - // This works because the bit pattern determines the encoding size, - // and we've handled negative int32 values as a special case above - return varint(static_cast(value)); - } - - /** - * @brief Calculates the size in bytes needed to encode a field ID and wire type - * - * @param field_id The field identifier - * @param type The wire type value (from the WireType enum in the protobuf spec) - * @return The number of bytes needed to encode the field ID and wire type - */ - static inline uint32_t field(uint32_t field_id, uint32_t type) { - uint32_t tag = (field_id << 3) | (type & 0b111); - return varint(tag); - } - - /** - * @brief Common parameters for all add_*_field methods - * - * All add_*_field methods follow these common patterns: - * - * @param total_size Reference to the total message size to update - * @param field_id_size Pre-calculated size of the field ID in bytes - * @param value The value to calculate size for (type varies) - * @param force Whether to calculate size even if the value is default/zero/empty - * - * Each method follows this implementation pattern: - * 1. Skip calculation if value is default (0, false, empty) and not forced - * 2. Calculate the size based on the field's encoding rules - * 3. Add the field_id_size + calculated value size to total_size - */ - - /** - * @brief Calculates and adds the size of an int32 field to the total message size - */ - static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) { - // Skip calculation if value is zero and not forced - if (value == 0 && !force) { - return; // No need to update total_size - } - - // Calculate and directly add to total_size - if (value < 0) { - // Negative values are encoded as 10-byte varints in protobuf - total_size += field_id_size + 10; - } else { - // For non-negative values, use the standard varint size - total_size += field_id_size + varint(static_cast(value)); - } - } - - /** - * @brief Calculates and adds the size of a uint32 field to the total message size - */ - static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, - bool force = false) { - // Skip calculation if value is zero and not forced - if (value == 0 && !force) { - return; // No need to update total_size - } - - // Calculate and directly add to total_size - total_size += field_id_size + varint(value); - } - - /** - * @brief Calculates and adds the size of a boolean field to the total message size - */ - static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value, bool force = false) { - // Skip calculation if value is false and not forced - if (!value && !force) { - return; // No need to update total_size - } - - // Boolean fields always use 1 byte when true - total_size += field_id_size + 1; - } - - /** - * @brief Calculates and adds the size of a fixed field to the total message size - * - * Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double). - * - * @tparam NumBytes The number of bytes for this fixed field (4 or 8) - * @param is_nonzero Whether the value is non-zero - */ - template - static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero, - bool force = false) { - // Skip calculation if value is zero and not forced - if (!is_nonzero && !force) { - return; // No need to update total_size - } - - // Fixed fields always take exactly NumBytes - total_size += field_id_size + NumBytes; - } - - /** - * @brief Calculates and adds the size of an enum field to the total message size - * - * Enum fields are encoded as uint32 varints. - */ - static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, bool force = false) { - // Skip calculation if value is zero and not forced - if (value == 0 && !force) { - return; // No need to update total_size - } - - // Enums are encoded as uint32 - total_size += field_id_size + varint(value); - } - - /** - * @brief Calculates and adds the size of a sint32 field to the total message size - * - * Sint32 fields use ZigZag encoding, which is more efficient for negative values. - */ - static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) { - // Skip calculation if value is zero and not forced - if (value == 0 && !force) { - return; // No need to update total_size - } - - // ZigZag encoding for sint32: (n << 1) ^ (n >> 31) - uint32_t zigzag = (static_cast(value) << 1) ^ (static_cast(value >> 31)); - total_size += field_id_size + varint(zigzag); - } - - /** - * @brief Calculates and adds the size of an int64 field to the total message size - */ - static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) { - // Skip calculation if value is zero and not forced - if (value == 0 && !force) { - return; // No need to update total_size - } - - // Calculate and directly add to total_size - total_size += field_id_size + varint(value); - } - - /** - * @brief Calculates and adds the size of a uint64 field to the total message size - */ - static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value, - bool force = false) { - // Skip calculation if value is zero and not forced - if (value == 0 && !force) { - return; // No need to update total_size - } - - // Calculate and directly add to total_size - 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, bool force = false) { - // Skip calculation if value is zero and not forced - if (value == 0 && !force) { - 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 string/bytes field to the total message size - */ - static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str, - bool force = false) { - // Skip calculation if string is empty and not forced - if (str.empty() && !force) { - return; // No need to update total_size - } - - // Calculate and directly add to total_size - const uint32_t str_size = static_cast(str.size()); - total_size += field_id_size + varint(str_size) + str_size; - } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size - * - * This helper function directly updates the total_size reference if the nested size - * is greater than zero or force is true. - * - * @param nested_size The pre-calculated size of the nested message - */ - static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size, - bool force = false) { - // Skip calculation if nested message is empty and not forced - if (nested_size == 0 && !force) { - return; // No need to update total_size - } - - // Calculate and directly add to total_size - // Field ID + length varint + nested message content - total_size += field_id_size + varint(nested_size) + nested_size; - } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size - * - * This version takes a ProtoMessage object, calculates its size internally, - * and updates the total_size reference. This eliminates the need for a temporary variable - * at the call site. - * - * @param message The nested message object - */ - static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message, - bool force = false) { - uint32_t nested_size = 0; - message.calculate_size(nested_size); - - // Use the base implementation with the calculated nested_size - add_message_field(total_size, field_id_size, nested_size, force); - } - - /** - * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size - * - * This helper processes a vector of message objects, calculating the size for each message - * and adding it to the total size. - * - * @tparam MessageType The type of the nested messages in the vector - * @param messages Vector of message objects - */ - template - static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size, - const std::vector &messages) { - // Skip if the vector is empty - if (messages.empty()) { - return; - } - - // For repeated fields, always use force=true - for (const auto &message : messages) { - add_message_object(total_size, field_id_size, message, true); - } - } -}; - -} // namespace api -} // namespace esphome diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 0915746381..88966089cc 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -24,14 +24,6 @@ static const char *const TAG = "api"; // APIServer APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#ifndef USE_API_YAML_SERVICES -// Global empty vector to avoid guard variables (saves 8 bytes) -// This is initialized at program startup before any threads -static const std::vector empty_user_services{}; - -const std::vector &get_empty_user_services_instance() { return empty_user_services; } -#endif - APIServer::APIServer() { global_api_server = this; // Pre-allocate shared write buffer @@ -39,7 +31,6 @@ APIServer::APIServer() { } void APIServer::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->setup_controller(); #ifdef USE_API_NOISE @@ -113,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); } }); @@ -193,9 +184,9 @@ void APIServer::loop() { // Rare case: handle disconnection #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); + this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername); #endif - ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); + ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str()); // Swap with the last element and pop (avoids expensive vector shifts) if (client_index < this->clients_.size() - 1) { @@ -213,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(); @@ -436,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); } }); } @@ -472,10 +462,12 @@ 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); + c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE, + DisconnectRequest::ESTIMATED_SIZE); } } } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index f34fd55974..edbd289421 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -12,7 +12,9 @@ #include "esphome/core/log.h" #include "list_entities.h" #include "subscribe_state.h" +#ifdef USE_API_SERVICES #include "user_services.h" +#endif #include @@ -25,11 +27,6 @@ struct SavedNoisePsk { } PACKED; // NOLINT #endif -#ifndef USE_API_YAML_SERVICES -// Forward declaration of helper function -const std::vector &get_empty_user_services_instance(); -#endif - class APIServer : public Component, public Controller { public: APIServer(); @@ -42,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); @@ -112,18 +108,9 @@ class APIServer : public Component, public Controller { void on_media_player_update(media_player::MediaPlayer *obj) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); - void register_user_service(UserServiceDescriptor *descriptor) { -#ifdef USE_API_YAML_SERVICES - // Vector is pre-allocated when services are defined in YAML - this->user_services_.push_back(descriptor); -#else - // Lazy allocate vector on first use for CustomAPIDevice - if (!this->user_services_) { - this->user_services_ = std::make_unique>(); - } - this->user_services_->push_back(descriptor); +#ifdef USE_API_SERVICES + void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #endif - } #ifdef USE_HOMEASSISTANT_TIME void request_time(); #endif @@ -152,17 +139,9 @@ class APIServer : public Component, public Controller { void get_home_assistant_state(std::string entity_id, optional attribute, std::function f); const std::vector &get_state_subs() const; - const std::vector &get_user_services() const { -#ifdef USE_API_YAML_SERVICES - return this->user_services_; -#else - if (this->user_services_) { - return *this->user_services_; - } - // Return reference to global empty instance (no guard needed) - return get_empty_user_services_instance(); +#ifdef USE_API_SERVICES + const std::vector &get_user_services() const { return this->user_services_; } #endif - } #ifdef USE_API_CLIENT_CONNECTED_TRIGGER Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } @@ -194,14 +173,8 @@ class APIServer : public Component, public Controller { #endif std::vector shared_write_buffer_; // Shared proto write buffer for all connections std::vector state_subs_; -#ifdef USE_API_YAML_SERVICES - // When services are defined in YAML, we know at compile time that services will be registered +#ifdef USE_API_SERVICES std::vector user_services_; -#else - // Services can still be registered at runtime by CustomAPIDevice components even when not - // defined in YAML. Using unique_ptr allows lazy allocation, saving 12 bytes in the common - // case where no services (YAML or custom) are used. - std::unique_ptr> user_services_; #endif // Group smaller types together diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 1a8e189f41..35329c4a5e 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -3,10 +3,13 @@ #include #include "api_server.h" #ifdef USE_API +#ifdef USE_API_SERVICES #include "user_services.h" +#endif namespace esphome { namespace api { +#ifdef USE_API_SERVICES template class CustomAPIDeviceService : public UserServiceBase { public: CustomAPIDeviceService(const std::string &name, const std::array &arg_names, T *obj, @@ -19,6 +22,7 @@ template class CustomAPIDeviceService : public UserS T *obj_; void (T::*callback_)(Ts...); }; +#endif // USE_API_SERVICES class CustomAPIDevice { public: @@ -46,12 +50,14 @@ class CustomAPIDevice { * @param name The name of the service to register. * @param arg_names The name of the arguments for the service, must match the arguments of the function. */ +#ifdef USE_API_SERVICES template void register_service(void (T::*callback)(Ts...), const std::string &name, const std::array &arg_names) { auto *service = new CustomAPIDeviceService(name, arg_names, (T *) this, callback); // NOLINT global_api_server->register_user_service(service); } +#endif /** Register a custom native API service that will show up in Home Assistant. * @@ -71,10 +77,12 @@ class CustomAPIDevice { * @param callback The member function to call when the service is triggered. * @param name The name of the arguments for the service, must match the arguments of the function. */ +#ifdef USE_API_SERVICES template void register_service(void (T::*callback)(), const std::string &name) { auto *service = new CustomAPIDeviceService(name, {}, (T *) this, callback); // NOLINT global_api_server->register_user_service(service); } +#endif /** Subscribe to the state (or attribute state) of an entity from Home Assistant. * 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 60814e359d..809c658803 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -83,10 +83,12 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done( ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} +#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 } // namespace api } // namespace esphome diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 4c83ca0935..b4cbf6c489 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -14,7 +14,7 @@ class APIConnection; #define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \ bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \ return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \ - ResponseType::MESSAGE_TYPE); \ + ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \ } class ListEntitiesIterator : public ComponentIterator { @@ -44,7 +44,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_TEXT_SENSOR bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif +#ifdef USE_API_SERVICES bool on_service(UserServiceDescriptor *service) override; +#endif #ifdef USE_CAMERA bool on_camera(camera::Camera *entity) override; #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 764bac2f39..a2c31100bf 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -4,6 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include #include #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE @@ -59,7 +60,6 @@ class ProtoVarInt { uint32_t as_uint32() const { return this->value_; } uint64_t as_uint64() const { return this->value_; } bool as_bool() const { return this->value_; } - template T as_enum() const { return static_cast(this->as_uint32()); } int32_t as_int32() const { // Not ZigZag encoded return static_cast(this->as_int64()); @@ -133,15 +133,25 @@ class ProtoVarInt { uint64_t value_; }; +// Forward declaration for decode_to_message and encode_to_writer +class ProtoMessage; +class ProtoDecodableMessage; + class ProtoLengthDelimited { public: explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {} std::string as_string() const { return std::string(reinterpret_cast(this->value_), this->length_); } - template C as_message() const { - auto msg = C(); - msg.decode(this->value_, this->length_); - return msg; - } + + /** + * 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 ProtoDecodableMessage's + * decode() method will be called with the raw data and length. + * + * @param msg The ProtoDecodableMessage instance to decode into + */ + void decode_to_message(ProtoDecodableMessage &msg) const; protected: const uint8_t *const value_; @@ -166,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: @@ -196,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 */ @@ -249,23 +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); - } - template void encode_enum(uint32_t field_id, T value, bool force = false) { - this->encode_uint32(field_id, static_cast(value), force); - } + // 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; @@ -306,18 +287,7 @@ class ProtoWriteBuffer { } this->encode_uint64(field_id, uvalue, force); } - template void encode_message(uint32_t field_id, const C &value, bool force = false) { - this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - size_t begin = this->buffer_->size(); - - value.encode(*this); - - const uint32_t nested_length = this->buffer_->size() - begin; - // add size varint - std::vector var; - ProtoVarInt(nested_length).encode(var); - this->buffer_->insert(this->buffer_->begin() + begin, var.begin(), var.end()); - } + void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false); std::vector *get_buffer() const { return buffer_; } protected: @@ -329,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 @@ -337,14 +306,519 @@ 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 { + public: + /** + * @brief ProtoSize class for Protocol Buffer serialization size calculation + * + * This class provides static methods to calculate the exact byte counts needed + * for encoding various Protocol Buffer field types. All methods are designed to be + * efficient for the common case where many fields have default values. + * + * Implements Protocol Buffer encoding size calculation according to: + * https://protobuf.dev/programming-guides/encoding/ + * + * Key features: + * - Early-return optimization for zero/default values + * - Direct total_size updates to avoid unnecessary additions + * - Specialized handling for different field types according to protobuf spec + * - Templated helpers for repeated fields and messages + */ + + /** + * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint + * + * @param value The uint32_t value to calculate size for + * @return The number of bytes needed to encode the value + */ + static inline uint32_t varint(uint32_t value) { + // Optimized varint size calculation using leading zeros + // Each 7 bits requires one byte in the varint encoding + if (value < 128) + return 1; // 7 bits, common case for small values + + // For larger values, count bytes needed based on the position of the highest bit set + if (value < 16384) { + return 2; // 14 bits + } else if (value < 2097152) { + return 3; // 21 bits + } else if (value < 268435456) { + return 4; // 28 bits + } else { + return 5; // 32 bits (maximum for uint32_t) + } + } + + /** + * @brief Calculates the size in bytes needed to encode a uint64_t value as a varint + * + * @param value The uint64_t value to calculate size for + * @return The number of bytes needed to encode the value + */ + static inline uint32_t varint(uint64_t value) { + // Handle common case of values fitting in uint32_t (vast majority of use cases) + if (value <= UINT32_MAX) { + return varint(static_cast(value)); + } + + // For larger values, determine size based on highest bit position + if (value < (1ULL << 35)) { + return 5; // 35 bits + } else if (value < (1ULL << 42)) { + return 6; // 42 bits + } else if (value < (1ULL << 49)) { + return 7; // 49 bits + } else if (value < (1ULL << 56)) { + return 8; // 56 bits + } else if (value < (1ULL << 63)) { + return 9; // 63 bits + } else { + return 10; // 64 bits (maximum for uint64_t) + } + } + + /** + * @brief Calculates the size in bytes needed to encode an int32_t value as a varint + * + * Special handling is needed for negative values, which are sign-extended to 64 bits + * in Protocol Buffers, resulting in a 10-byte varint. + * + * @param value The int32_t value to calculate size for + * @return The number of bytes needed to encode the value + */ + static inline uint32_t varint(int32_t value) { + // Negative values are sign-extended to 64 bits in protocol buffers, + // which always results in a 10-byte varint for negative int32 + if (value < 0) { + return 10; // Negative int32 is always 10 bytes long + } + // For non-negative values, use the uint32_t implementation + return varint(static_cast(value)); + } + + /** + * @brief Calculates the size in bytes needed to encode an int64_t value as a varint + * + * @param value The int64_t value to calculate size for + * @return The number of bytes needed to encode the value + */ + static inline uint32_t varint(int64_t value) { + // For int64_t, we convert to uint64_t and calculate the size + // This works because the bit pattern determines the encoding size, + // and we've handled negative int32 values as a special case above + return varint(static_cast(value)); + } + + /** + * @brief Calculates the size in bytes needed to encode a field ID and wire type + * + * @param field_id The field identifier + * @param type The wire type value (from the WireType enum in the protobuf spec) + * @return The number of bytes needed to encode the field ID and wire type + */ + static inline uint32_t field(uint32_t field_id, uint32_t type) { + uint32_t tag = (field_id << 3) | (type & 0b111); + return varint(tag); + } + + /** + * @brief Common parameters for all add_*_field methods + * + * All add_*_field methods follow these common patterns: + * + * @param total_size Reference to the total message size to update + * @param field_id_size Pre-calculated size of the field ID in bytes + * @param value The value to calculate size for (type varies) + * @param force Whether to calculate size even if the value is default/zero/empty + * + * Each method follows this implementation pattern: + * 1. Skip calculation if value is default (0, false, empty) and not forced + * 2. Calculate the size based on the field's encoding rules + * 3. Add the field_id_size + calculated value size to total_size + */ + + /** + * @brief Calculates and adds the size of an int32 field to the total message size + */ + static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) { + // Skip calculation if value is zero + if (value == 0) { + return; // No need to update total_size + } + + // Calculate and directly add to total_size + if (value < 0) { + // Negative values are encoded as 10-byte varints in protobuf + total_size += field_id_size + 10; + } else { + // For non-negative values, use the standard varint size + total_size += field_id_size + varint(static_cast(value)); + } + } + + /** + * @brief Calculates and adds the size of an int32 field to the total message size (repeated field version) + */ + static inline void add_int32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) { + // Always calculate size for repeated fields + if (value < 0) { + // Negative values are encoded as 10-byte varints in protobuf + total_size += field_id_size + 10; + } else { + // For non-negative values, use the standard varint size + total_size += field_id_size + varint(static_cast(value)); + } + } + + /** + * @brief Calculates and adds the size of a uint32 field to the total message size + */ + static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { + // Skip calculation if value is zero + if (value == 0) { + return; // No need to update total_size + } + + // Calculate and directly add to total_size + total_size += field_id_size + varint(value); + } + + /** + * @brief Calculates and adds the size of a uint32 field to the total message size (repeated field version) + */ + static inline void add_uint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { + // Always calculate size for repeated fields + total_size += field_id_size + varint(value); + } + + /** + * @brief Calculates and adds the size of a boolean field to the total message size + */ + static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value) { + // Skip calculation if value is false + if (!value) { + return; // No need to update total_size + } + + // Boolean fields always use 1 byte when true + total_size += field_id_size + 1; + } + + /** + * @brief Calculates and adds the size of a boolean field to the total message size (repeated field version) + */ + static inline void add_bool_field_repeated(uint32_t &total_size, uint32_t field_id_size, bool value) { + // Always calculate size for repeated fields + // Boolean fields always use 1 byte + total_size += field_id_size + 1; + } + + /** + * @brief Calculates and adds the size of a fixed field to the total message size + * + * Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double). + * + * @tparam NumBytes The number of bytes for this fixed field (4 or 8) + * @param is_nonzero Whether the value is non-zero + */ + template + static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero) { + // Skip calculation if value is zero + if (!is_nonzero) { + return; // No need to update total_size + } + + // Fixed fields always take exactly NumBytes + 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 + * + * Enum fields are encoded as uint32 varints. + */ + static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { + // Skip calculation if value is zero + if (value == 0) { + return; // No need to update total_size + } + + // Enums are encoded as uint32 + total_size += field_id_size + varint(value); + } + + /** + * @brief Calculates and adds the size of an enum field to the total message size (repeated field version) + * + * Enum fields are encoded as uint32 varints. + */ + static inline void add_enum_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { + // Always calculate size for repeated fields + // Enums are encoded as uint32 + total_size += field_id_size + varint(value); + } + + /** + * @brief Calculates and adds the size of a sint32 field to the total message size + * + * Sint32 fields use ZigZag encoding, which is more efficient for negative values. + */ + static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) { + // Skip calculation if value is zero + if (value == 0) { + return; // No need to update total_size + } + + // ZigZag encoding for sint32: (n << 1) ^ (n >> 31) + uint32_t zigzag = (static_cast(value) << 1) ^ (static_cast(value >> 31)); + total_size += field_id_size + varint(zigzag); + } + + /** + * @brief Calculates and adds the size of a sint32 field to the total message size (repeated field version) + * + * Sint32 fields use ZigZag encoding, which is more efficient for negative values. + */ + static inline void add_sint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) { + // Always calculate size for repeated fields + // ZigZag encoding for sint32: (n << 1) ^ (n >> 31) + uint32_t zigzag = (static_cast(value) << 1) ^ (static_cast(value >> 31)); + total_size += field_id_size + varint(zigzag); + } + + /** + * @brief Calculates and adds the size of an int64 field to the total message size + */ + static inline void add_int64_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 + } + + // Calculate and directly add to total_size + total_size += field_id_size + varint(value); + } + + /** + * @brief Calculates and adds the size of an int64 field to the total message size (repeated field version) + */ + static inline void add_int64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) { + // Always calculate size for repeated fields + total_size += field_id_size + varint(value); + } + + /** + * @brief Calculates and adds the size of a uint64 field to the total message size + */ + static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value) { + // Skip calculation if value is zero + if (value == 0) { + return; // No need to update total_size + } + + // Calculate and directly add to total_size + total_size += field_id_size + varint(value); + } + + /** + * @brief Calculates and adds the size of a uint64 field to the total message size (repeated field version) + */ + static inline void add_uint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint64_t value) { + // Always calculate size for repeated fields + total_size += field_id_size + varint(value); + } + + // 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 + */ + static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str) { + // Skip calculation if string is empty + if (str.empty()) { + return; // No need to update total_size + } + + // Calculate and directly add to total_size + const uint32_t str_size = static_cast(str.size()); + total_size += field_id_size + varint(str_size) + str_size; + } + + /** + * @brief Calculates and adds the size of a string/bytes field to the total message size (repeated field version) + */ + static inline void add_string_field_repeated(uint32_t &total_size, uint32_t field_id_size, const std::string &str) { + // Always calculate size for repeated fields + const uint32_t str_size = static_cast(str.size()); + total_size += field_id_size + varint(str_size) + str_size; + } + + /** + * @brief Calculates and adds the size of a nested message field to the total message size + * + * This helper function directly updates the total_size reference if the nested size + * is greater than zero. + * + * @param nested_size The pre-calculated size of the nested message + */ + static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) { + // Skip calculation if nested message is empty + if (nested_size == 0) { + return; // No need to update total_size + } + + // Calculate and directly add to total_size + // Field ID + length varint + nested message content + total_size += field_id_size + varint(nested_size) + nested_size; + } + + /** + * @brief Calculates and adds the size of a nested message field to the total message size (repeated field version) + * + * @param nested_size The pre-calculated size of the nested message + */ + static inline void add_message_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) { + // Always calculate size for repeated fields + // Field ID + length varint + nested message content + total_size += field_id_size + varint(nested_size) + nested_size; + } + + /** + * @brief Calculates and adds the size of a nested message field to the total message size + * + * This version takes a ProtoMessage object, calculates its size internally, + * and updates the total_size reference. This eliminates the need for a temporary variable + * at the call site. + * + * @param message The nested message object + */ + static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message) { + uint32_t nested_size = 0; + message.calculate_size(nested_size); + + // Use the base implementation with the calculated nested_size + add_message_field(total_size, field_id_size, nested_size); + } + + /** + * @brief Calculates and adds the size of a nested message field to the total message size (repeated field version) + * + * @param message The nested message object + */ + static inline void add_message_object_repeated(uint32_t &total_size, uint32_t field_id_size, + const ProtoMessage &message) { + uint32_t nested_size = 0; + message.calculate_size(nested_size); + + // Use the base implementation with the calculated nested_size + add_message_field_repeated(total_size, field_id_size, nested_size); + } + + /** + * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size + * + * This helper processes a vector of message objects, calculating the size for each message + * and adding it to the total size. + * + * @tparam MessageType The type of the nested messages in the vector + * @param messages Vector of message objects + */ + template + static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size, + const std::vector &messages) { + // Skip if the vector is empty + if (messages.empty()) { + return; + } + + // Use the repeated field version for all messages + for (const auto &message : messages) { + add_message_object_repeated(total_size, field_id_size, message); + } + } +}; + +// Implementation of encode_message - must be after ProtoMessage is defined +inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { + this->encode_field_raw(field_id, 2); // type 2: Length-delimited message + + // Calculate the message size first + uint32_t msg_length_bytes = 0; + value.calculate_size(msg_length_bytes); + + // Calculate how many bytes the length varint needs + uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes); + + // Reserve exact space for the length varint + size_t begin = this->buffer_->size(); + this->buffer_->resize(this->buffer_->size() + varint_length_bytes); + + // Write the length varint directly + ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes); + + // Now encode the message content - it will append to the buffer + value.encode(*this); + + // Verify that the encoded size matches what we calculated + assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); +} + +// 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_); +} + template const char *proto_enum_to_string(T value); class ProtoService { @@ -363,11 +837,11 @@ class ProtoService { * @return A ProtoWriteBuffer object with the reserved size. */ virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; - virtual bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) = 0; + virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; // Optimized method that pre-allocates buffer based on message size - bool send_message_(const ProtoMessage &msg, uint16_t message_type) { + bool send_message_(const ProtoMessage &msg, uint8_t message_type) { uint32_t msg_size = 0; msg.calculate_size(msg_size); diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 673bcf5693..1420a15ff9 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -7,6 +7,7 @@ #include "esphome/core/automation.h" #include "api_pb2.h" +#ifdef USE_API_SERVICES namespace esphome { namespace api { @@ -15,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); @@ -73,3 +76,4 @@ template class UserServiceTrigger : public UserServiceBase get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { + esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid(); + return std::vector{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | + ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | + ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | + ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]), + ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | + ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | + ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | + ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])}; +} + void BluetoothConnection::dump_config() { ESP_LOGCONFIG(TAG, "BLE Connection:"); BLEClientBase::dump_config(); } +void BluetoothConnection::loop() { + BLEClientBase::loop(); + + // Early return if no active connection or not in service discovery phase + if (this->address_ == 0 || this->send_service_ < 0 || this->send_service_ > this->service_count_) { + return; + } + + // Handle service discovery + this->send_service_for_discovery_(); +} + +void BluetoothConnection::reset_connection_(esp_err_t reason) { + // Send disconnection notification + this->proxy_->send_device_connection(this->address_, false, 0, reason); + + // Important: If we were in the middle of sending services, we do NOT send + // send_gatt_services_done() here. This ensures the client knows that + // the service discovery was interrupted and can retry. The client + // (aioesphomeapi) implements a 30-second timeout (DEFAULT_BLE_TIMEOUT) + // to detect incomplete service discovery rather than relying on us to + // tell them about a partial list. + this->set_address(0); + this->send_service_ = DONE_SENDING_SERVICES; + this->proxy_->send_connections_free(); +} + +void BluetoothConnection::send_service_for_discovery_() { + if (this->send_service_ == this->service_count_) { + this->send_service_ = DONE_SENDING_SERVICES; + this->proxy_->send_gatt_services_done(this->address_); + if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || + this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + this->release_services(); + } + return; + } + + // Early return if no API connection + auto *api_conn = this->proxy_->get_api_connection(); + if (api_conn == nullptr) { + return; + } + + // Send next service + esp_gattc_service_elem_t service_result; + uint16_t service_count = 1; + esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, + &service_result, &service_count, this->send_service_); + this->send_service_++; + + if (service_status != ESP_GATT_OK) { + ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d", this->connection_index_, + this->address_str().c_str(), this->send_service_ - 1, service_status); + return; + } + + if (service_count == 0) { + ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d", this->connection_index_, + this->address_str().c_str(), service_count); + return; + } + + api::BluetoothGATTGetServicesResponse resp; + resp.address = this->address_; + resp.services.emplace_back(); + auto &service_resp = resp.services.back(); + service_resp.uuid = get_128bit_uuid_vec(service_result.uuid); + service_resp.handle = service_result.start_handle; + + // Get the number of characteristics directly with one call + uint16_t total_char_count = 0; + esp_gatt_status_t char_count_status = + esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC, + service_result.start_handle, service_result.end_handle, 0, &total_char_count); + + if (char_count_status == ESP_GATT_OK && total_char_count > 0) { + // Only reserve if we successfully got a count + service_resp.characteristics.reserve(total_char_count); + } else if (char_count_status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_, + this->address_str().c_str(), char_count_status); + } + + // Now process characteristics + uint16_t char_offset = 0; + esp_gattc_char_elem_t char_result; + while (true) { // characteristics + uint16_t char_count = 1; + esp_gatt_status_t char_status = + esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle, + service_result.end_handle, &char_result, &char_count, char_offset); + if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) { + break; + } + if (char_status != ESP_GATT_OK) { + ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, + this->address_str().c_str(), char_status); + break; + } + if (char_count == 0) { + break; + } + + service_resp.characteristics.emplace_back(); + auto &characteristic_resp = service_resp.characteristics.back(); + characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid); + characteristic_resp.handle = char_result.char_handle; + characteristic_resp.properties = char_result.properties; + char_offset++; + + // Get the number of descriptors directly with one call + uint16_t total_desc_count = 0; + esp_gatt_status_t desc_count_status = + esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, char_result.char_handle, + service_result.end_handle, 0, &total_desc_count); + + if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) { + // Only reserve if we successfully got a count + characteristic_resp.descriptors.reserve(total_desc_count); + } else if (desc_count_status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_, + this->address_str().c_str(), char_result.char_handle, desc_count_status); + } + + // Now process descriptors + uint16_t desc_offset = 0; + esp_gattc_descr_elem_t desc_result; + while (true) { // descriptors + uint16_t desc_count = 1; + esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr( + this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset); + if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) { + break; + } + if (desc_status != ESP_GATT_OK) { + ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_, + this->address_str().c_str(), desc_status); + break; + } + if (desc_count == 0) { + break; + } + + characteristic_resp.descriptors.emplace_back(); + auto &descriptor_resp = characteristic_resp.descriptors.back(); + descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid); + descriptor_resp.handle = desc_result.handle; + desc_offset++; + } + } + + // Send the message (we already checked api_conn is not null at the beginning) + api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); +} + bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { if (!BLEClientBase::gattc_event_handler(event, gattc_if, param)) @@ -25,22 +193,16 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga switch (event) { case ESP_GATTC_DISCONNECT_EVT: { - this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); - this->set_address(0); - this->proxy_->send_connections_free(); + this->reset_connection_(param->disconnect.reason); break; } case ESP_GATTC_CLOSE_EVT: { - this->proxy_->send_device_connection(this->address_, false, 0, param->close.reason); - this->set_address(0); - this->proxy_->send_connections_free(); + this->reset_connection_(param->close.reason); break; } case ESP_GATTC_OPEN_EVT: { if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { - this->proxy_->send_device_connection(this->address_, false, 0, param->open.status); - this->set_address(0); - this->proxy_->send_connections_free(); + this->reset_connection_(param->open.status); } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { this->proxy_->send_device_connection(this->address_, true, this->mtu_); this->proxy_->send_connections_free(); @@ -75,7 +237,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 +251,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 +265,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 +278,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 +290,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_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 73c034d93b..2673238fba 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -12,6 +12,7 @@ class BluetoothProxy; class BluetoothConnection : public esp32_ble_client::BLEClientBase { public: void dump_config() override; + void loop() 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; void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; @@ -27,6 +28,9 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { protected: friend class BluetoothProxy; + void send_service_for_discovery_(); + void reset_connection_(esp_err_t reason); + // Memory optimized layout for 32-bit systems // Group 1: Pointers (4 bytes each, naturally aligned) BluetoothProxy *proxy_; diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index a5e8ec0860..8a1a2bff6a 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 @@ -10,23 +11,31 @@ namespace esphome { namespace bluetooth_proxy { static const char *const TAG = "bluetooth_proxy"; -static const int DONE_SENDING_SERVICES = -2; -std::vector get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { - esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid(); - return std::vector{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | - ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | - ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | - ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]), - ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | - ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | - ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | - ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])}; -} +// 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,128 +48,91 @@ 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; -void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { - api::BluetoothLEAdvertisementResponse resp; - resp.address = device.address_uint64(); - resp.address_type = device.get_address_type(); - if (!device.get_name().empty()) - resp.name = device.get_name(); - resp.rssi = device.get_rssi(); + // 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())); - // Pre-allocate vectors based on known sizes - auto service_uuids = device.get_service_uuids(); - resp.service_uuids.reserve(service_uuids.size()); - for (auto &uuid : service_uuids) { - resp.service_uuids.emplace_back(uuid.to_string()); + // Resize to actual count + advertisements.resize(this->advertisement_count_); } - // Pre-allocate service data vector - auto service_datas = device.get_service_datas(); - resp.service_data.reserve(service_datas.size()); - for (auto &data : service_datas) { - resp.service_data.emplace_back(); - auto &service_data = resp.service_data.back(); - service_data.uuid = data.uuid.to_string(); - service_data.data.assign(data.data.begin(), data.data.end()); - } + // Send the message + this->api_connection_->send_message(*this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); - // Pre-allocate manufacturer data vector - auto manufacturer_datas = device.get_manufacturer_datas(); - resp.manufacturer_data.reserve(manufacturer_datas.size()); - for (auto &data : manufacturer_datas) { - resp.manufacturer_data.emplace_back(); - auto &manufacturer_data = resp.manufacturer_data.back(); - manufacturer_data.uuid = data.uuid.to_string(); - manufacturer_data.data.assign(data.data.begin(), data.data.end()); - } - - this->api_connection_->send_message(resp); + // Reset count - existing items will be overwritten in next batch + this->advertisement_count_ = 0; } 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,139 +160,17 @@ 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(); + 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; - } - } - for (auto *connection : this->connections_) { - if (connection->send_service_ == connection->service_count_) { - connection->send_service_ = DONE_SENDING_SERVICES; - this->send_gatt_services_done(connection->get_address()); - if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || - connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { - connection->release_services(); - } - } else if (connection->send_service_ >= 0) { - esp_gattc_service_elem_t service_result; - uint16_t service_count = 1; - esp_gatt_status_t service_status = - esp_ble_gattc_get_service(connection->get_gattc_if(), connection->get_conn_id(), nullptr, &service_result, - &service_count, connection->send_service_); - connection->send_service_++; - if (service_status != ESP_GATT_OK) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d", - connection->get_connection_index(), connection->address_str().c_str(), connection->send_service_ - 1, - service_status); - continue; - } - if (service_count == 0) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d", - connection->get_connection_index(), connection->address_str().c_str(), service_count); - continue; - } - api::BluetoothGATTGetServicesResponse resp; - resp.address = connection->get_address(); - resp.services.reserve(1); // Always one service per response in this implementation - api::BluetoothGATTService service_resp; - service_resp.uuid = get_128bit_uuid_vec(service_result.uuid); - service_resp.handle = service_result.start_handle; - uint16_t char_offset = 0; - esp_gattc_char_elem_t char_result; - // Get the number of characteristics directly with one call - uint16_t total_char_count = 0; - esp_gatt_status_t char_count_status = esp_ble_gattc_get_attr_count( - connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_CHARACTERISTIC, - service_result.start_handle, service_result.end_handle, 0, &total_char_count); - - if (char_count_status == ESP_GATT_OK && total_char_count > 0) { - // Only reserve if we successfully got a count - service_resp.characteristics.reserve(total_char_count); - } else if (char_count_status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", connection->get_connection_index(), - connection->address_str().c_str(), char_count_status); - } - - // Now process characteristics - while (true) { // characteristics - uint16_t char_count = 1; - esp_gatt_status_t char_status = esp_ble_gattc_get_all_char( - connection->get_gattc_if(), connection->get_conn_id(), service_result.start_handle, - service_result.end_handle, &char_result, &char_count, char_offset); - if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) { - break; - } - if (char_status != ESP_GATT_OK) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", connection->get_connection_index(), - connection->address_str().c_str(), char_status); - break; - } - if (char_count == 0) { - break; - } - api::BluetoothGATTCharacteristic characteristic_resp; - characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid); - characteristic_resp.handle = char_result.char_handle; - characteristic_resp.properties = char_result.properties; - char_offset++; - - // Get the number of descriptors directly with one call - uint16_t total_desc_count = 0; - esp_gatt_status_t desc_count_status = - esp_ble_gattc_get_attr_count(connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_DESCRIPTOR, - char_result.char_handle, service_result.end_handle, 0, &total_desc_count); - - if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) { - // Only reserve if we successfully got a count - characteristic_resp.descriptors.reserve(total_desc_count); - } else if (desc_count_status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", - connection->get_connection_index(), connection->address_str().c_str(), char_result.char_handle, - desc_count_status); - } - - // Now process descriptors - uint16_t desc_offset = 0; - esp_gattc_descr_elem_t desc_result; - while (true) { // descriptors - uint16_t desc_count = 1; - esp_gatt_status_t desc_status = - esp_ble_gattc_get_all_descr(connection->get_gattc_if(), connection->get_conn_id(), - char_result.char_handle, &desc_result, &desc_count, desc_offset); - if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) { - break; - } - if (desc_status != ESP_GATT_OK) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", connection->get_connection_index(), - connection->address_str().c_str(), desc_status); - break; - } - if (desc_count == 0) { - break; - } - api::BluetoothGATTDescriptor descriptor_resp; - descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid); - descriptor_resp.handle = desc_result.handle; - characteristic_resp.descriptors.push_back(std::move(descriptor_resp)); - desc_offset++; - } - service_resp.characteristics.push_back(std::move(characteristic_resp)); - } - resp.services.push_back(std::move(service_resp)); - this->api_connection_->send_message(resp); - } + // Flush accumulated advertisements every 100ms + if (now - this->last_advertisement_flush_time_ >= 100) { + this->flush_pending_advertisements(); + this->last_advertisement_flush_time_ = now; } } 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 +315,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 +415,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 +426,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 +437,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 +450,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 +458,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 +468,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 +477,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 +486,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..b3d9044a2c 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -22,6 +22,7 @@ namespace esphome { namespace bluetooth_proxy { static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; +static const int DONE_SENDING_SERVICES = -2; using namespace esp32_ble_client; @@ -51,7 +52,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 +132,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com } protected: - void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); BluetoothConnection *get_connection_(uint64_t address, bool reserve); @@ -141,9 +143,16 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com // Group 2: Container types (typically 12 bytes on 32-bit) std::vector connections_{}; - // Group 3: 1-byte types grouped together + // BLE advertisement batching + std::vector advertisement_pool_; + std::unique_ptr response_; + + // Group 3: 4-byte types + uint32_t last_advertisement_flush_time_{0}; + + // Group 4: 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/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index e48a4941b3..37990aeec5 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -53,6 +53,7 @@ void DebugComponent::on_shutdown() { auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); if (component != nullptr) { strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1); + buffer[REBOOT_MAX_LEN - 1] = '\0'; } ESP_LOGD(TAG, "Storing reboot source: %s", buffer); pref.save(&buffer); @@ -68,6 +69,7 @@ std::string DebugComponent::get_reset_reason_() { auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); char buffer[REBOOT_MAX_LEN]{}; if (pref.load(&buffer)) { + buffer[REBOOT_MAX_LEN - 1] = '\0'; reset_reason = "Reboot request from " + std::string(buffer); } } diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 55826f52bb..05ae60239d 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -1,6 +1,6 @@ from esphome import automation, pins import esphome.codegen as cg -from esphome.components import time +from esphome.components import esp32, time from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, @@ -116,12 +116,20 @@ def validate_pin_number(value): return value -def validate_config(config): - if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config: - raise cv.Invalid("ESP32-C3 does not support wakeup from touch.") - if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config: - raise cv.Invalid("ESP32-C3 does not support wakeup from ext1") - return config +def _validate_ex1_wakeup_mode(value): + if value == "ALL_LOW": + esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value) + if value == "ANY_LOW": + esp32.only_on_variant( + supported=[ + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + ], + msg_prefix="ANY_LOW", + )(value) + return value deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") @@ -148,6 +156,7 @@ WAKEUP_PIN_MODES = { esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t") Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup") EXT1_WAKEUP_MODES = { + "ANY_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_LOW, "ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW, "ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH, } @@ -187,16 +196,28 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( cv.only_on_esp32, + esp32.only_on_variant( + unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1" + ), cv.Schema( { cv.Required(CONF_PINS): cv.ensure_list( pins.internal_gpio_input_pin_schema, validate_pin_number ), - cv.Required(CONF_MODE): cv.enum(EXT1_WAKEUP_MODES, upper=True), + cv.Required(CONF_MODE): cv.All( + cv.enum(EXT1_WAKEUP_MODES, upper=True), + _validate_ex1_wakeup_mode, + ), } ), ), - cv.Optional(CONF_TOUCH_WAKEUP): cv.All(cv.only_on_esp32, cv.boolean), + cv.Optional(CONF_TOUCH_WAKEUP): cv.All( + cv.only_on_esp32, + esp32.only_on_variant( + unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from touch" + ), + cv.boolean, + ), } ).extend(cv.COMPONENT_SCHEMA), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), 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 d1745b72bb..587d75b64b 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -31,6 +31,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_ESP32, + CoreModel, __version__, ) from esphome.core import CORE, HexInt, TimePeriod @@ -39,7 +40,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, @@ -189,7 +190,7 @@ def get_download_types(storage_json): ] -def only_on_variant(*, supported=None, unsupported=None): +def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This feature"): """Config validator for features only available on some ESP32 variants.""" if supported is not None and not isinstance(supported, list): supported = [supported] @@ -200,11 +201,11 @@ def only_on_variant(*, supported=None, unsupported=None): variant = get_esp32_variant() if supported is not None and variant not in supported: raise cv.Invalid( - f"This feature is only available on {', '.join(supported)}" + f"{msg_prefix} is only available on {', '.join(supported)}" ) if unsupported is not None and variant in unsupported: raise cv.Invalid( - f"This feature is not available on {', '.join(unsupported)}" + f"{msg_prefix} is not available on {', '.join(unsupported)}" ) return obj @@ -487,25 +488,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 +684,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 +699,7 @@ CONFIG_SCHEMA = cv.All( _detect_variant, _set_default_framework, set_core_data, + cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT), ) @@ -705,8 +714,10 @@ async def to_code(config): cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) + cg.add_define(CoreModel.MULTI_ATOMICS) cg.add_platformio_option("lib_ldf_mode", "off") + cg.add_platformio_option("lib_compat_mode", "strict") framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] 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/gpio.cpp b/esphome/components/esp32/gpio.cpp index b554b6d09c..27572063ca 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -114,7 +114,6 @@ void ESP32InternalGPIOPin::setup() { if (flags_ & gpio::FLAG_OUTPUT) { gpio_set_drive_capability(pin_, drive_strength_); } - ESP_LOGD(TAG, "rtc: %d", SOC_GPIO_SUPPORT_RTC_INDEPENDENT); } void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { 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..96003073d7 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -128,44 +128,53 @@ void ESP32BLETracker::loop() { uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); while (read_idx != write_idx) { - // Process one result at a time directly from ring buffer - BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx]; + // Calculate how many contiguous results we can process in one batch + // If write > read: process all results from read to write + // If write <= read (wraparound): process from read to end of buffer first + size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx); + // Process the batch for raw advertisements if (this->raw_advertisements_) { for (auto *listener : this->listeners_) { - listener->parse_devices(&scan_result, 1); + listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size); } for (auto *client : this->clients_) { - client->parse_devices(&scan_result, 1); + client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size); } } + // Process individual results for parsed advertisements if (this->parse_advertisements_) { - ESPBTDevice device; - device.parse_scan_rst(scan_result); +#ifdef USE_ESP32_BLE_DEVICE + for (size_t i = 0; i < batch_size; i++) { + BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i]; + ESPBTDevice device; + device.parse_scan_rst(scan_result); - bool found = false; - for (auto *listener : this->listeners_) { - if (listener->parse_device(device)) - found = true; - } + bool found = false; + for (auto *listener : this->listeners_) { + if (listener->parse_device(device)) + found = true; + } - for (auto *client : this->clients_) { - if (client->parse_device(device)) { - found = true; - if (!connecting && client->state() == ClientState::DISCOVERED) { - promote_to_connecting = true; + for (auto *client : this->clients_) { + if (client->parse_device(device)) { + found = true; + if (!connecting && client->state() == ClientState::DISCOVERED) { + promote_to_connecting = true; + } } } - } - if (!found && !this->scan_continuous_) { - this->print_bt_device_info(device); + if (!found && !this->scan_continuous_) { + this->print_bt_device_info(device); + } } +#endif // USE_ESP32_BLE_DEVICE } - // Move to next entry in ring buffer - read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE; + // Update read index for entire batch + read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE; // Store with release to ensure reads complete before index update this->ring_read_index_.store(read_idx, std::memory_order_release); @@ -511,6 +520,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 +761,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 +809,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 +880,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 138f318a5d..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,9 +339,10 @@ 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_ESP32_CAMERA") + cg.add_define("USE_CAMERA") if CORE.using_esp_idf: add_idf_component(name="espressif/esp32-camera", ref="2.0.15") 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/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp index 6f05610ed6..c3d43c6bbf 100644 --- a/esphome/components/esp32_touch/esp32_touch_v1.cpp +++ b/esphome/components/esp32_touch/esp32_touch_v1.cpp @@ -109,6 +109,7 @@ void ESP32TouchComponent::loop() { // Only publish if state changed - this filters out repeated events if (new_state != child->last_state_) { + child->initial_state_published_ = true; child->last_state_ = new_state; child->publish_state(new_state); // Original ESP32: ISR only fires when touched, release is detected by timeout @@ -175,6 +176,9 @@ void ESP32TouchComponent::on_shutdown() { void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { ESP32TouchComponent *component = static_cast(arg); + uint32_t mask = 0; + touch_ll_read_trigger_status_mask(&mask); + touch_ll_clear_trigger_status_mask(); touch_pad_clear_status(); // INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured @@ -184,6 +188,11 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { // as any pad remains touched. This allows us to detect both new touches and // continued touches, but releases must be detected by timeout in the main loop. + // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! + // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE + // Therefore: touched = (value < threshold) + // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) + // Process all configured pads to check their current state // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, // so we must scan all configured pads to find which ones were touched @@ -201,19 +210,12 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { value = touch_ll_read_raw_data(pad); } - // Skip pads with 0 value - they haven't been measured in this cycle - // This is important: not all pads are measured every interrupt cycle, - // only those that the hardware has updated - if (value == 0) { + // Skip pads that aren’t in the trigger mask + bool is_touched = (mask >> pad) & 1; + if (!is_touched) { continue; } - // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! - // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE - // Therefore: touched = (value < threshold) - // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) - bool is_touched = value < child->get_threshold(); - // Always send the current state - the main loop will filter for changes // We send both touched and untouched states because the ISR doesn't // track previous state (to keep ISR fast and simple) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 81daad8c56..d08d7121b7 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_ESP8266, + CoreModel, ) from esphome.core import CORE, coroutine_with_priority from esphome.helpers import copy_file_if_changed @@ -180,12 +181,14 @@ async def to_code(config): cg.add(esp8266_ns.setup_preferences()) cg.add_platformio_option("lib_ldf_mode", "off") + cg.add_platformio_option("lib_compat_mode", "strict") cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_ESP8266") cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "ESP8266") + cg.add_define(CoreModel.SINGLE) cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) 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/esp_ldo/__init__.py b/esphome/components/esp_ldo/__init__.py index ce24028083..38e684c537 100644 --- a/esphome/components/esp_ldo/__init__.py +++ b/esphome/components/esp_ldo/__init__.py @@ -20,14 +20,16 @@ adjusted_ids = set() CONFIG_SCHEMA = cv.All( cv.ensure_list( - { - cv.GenerateID(): cv.declare_id(EspLdo), - cv.Required(CONF_VOLTAGE): cv.All( - cv.voltage, cv.float_range(min=0.5, max=2.7) - ), - cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), - cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, - } + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EspLdo), + cv.Required(CONF_VOLTAGE): cv.All( + cv.voltage, cv.float_range(min=0.5, max=2.7) + ), + cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), + cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, + } + ) ), cv.only_with_esp_idf, only_on_variant(supported=[VARIANT_ESP32P4]), diff --git a/esphome/components/esp_ldo/esp_ldo.h b/esphome/components/esp_ldo/esp_ldo.h index fb5abf7a3a..bafa32db6b 100644 --- a/esphome/components/esp_ldo/esp_ldo.h +++ b/esphome/components/esp_ldo/esp_ldo.h @@ -17,6 +17,9 @@ class EspLdo : public Component { void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; } void set_voltage(float voltage) { this->voltage_ = voltage; } void adjust_voltage(float voltage); + float get_setup_priority() const override { + return setup_priority::BUS; // LDO setup should be done early + } protected: int channel_; diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index ac07d02e37..619346b914 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -342,5 +342,11 @@ async def to_code(config): cg.add_define("USE_ETHERNET") + # Disable WiFi when using Ethernet to save memory + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False) + # Also disable WiFi/BT coexistence since WiFi is disabled + add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False) + if CORE.using_arduino: cg.add_library("WiFi", None) 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/fan/fan.cpp b/esphome/components/fan/fan.cpp index 25f710f893..82fc5319e0 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -177,6 +177,10 @@ optional Fan::restore_state_() { return {}; } void Fan::save_state_() { + if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) { + return; + } + FanRestoreState state{}; state.state = this->state; state.oscillating = this->oscillating; diff --git a/esphome/components/gl_r01_i2c/__init__.py b/esphome/components/gl_r01_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp new file mode 100644 index 0000000000..5a24c63525 --- /dev/null +++ b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp @@ -0,0 +1,68 @@ +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include "gl_r01_i2c.h" + +namespace esphome { +namespace gl_r01_i2c { + +static const char *const TAG = "gl_r01_i2c"; + +// Register definitions from datasheet +static const uint8_t REG_VERSION = 0x00; +static const uint8_t REG_DISTANCE = 0x02; +static const uint8_t REG_TRIGGER = 0x10; +static const uint8_t CMD_TRIGGER = 0xB0; +static const uint8_t RESTART_CMD1 = 0x5A; +static const uint8_t RESTART_CMD2 = 0xA5; +static const uint8_t READ_DELAY = 40; // minimum milliseconds from datasheet to safely read measurement result + +void GLR01I2CComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up GL-R01 I2C..."); + // Verify sensor presence + if (!this->read_byte_16(REG_VERSION, &this->version_)) { + ESP_LOGE(TAG, "Failed to communicate with GL-R01 I2C sensor!"); + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "Found GL-R01 I2C with version 0x%04X", this->version_); +} + +void GLR01I2CComponent::dump_config() { + ESP_LOGCONFIG(TAG, "GL-R01 I2C:"); + ESP_LOGCONFIG(TAG, " Firmware Version: 0x%04X", this->version_); + LOG_I2C_DEVICE(this); + LOG_SENSOR(" ", "Distance", this); +} + +void GLR01I2CComponent::update() { + // Trigger a new measurement + if (!this->write_byte(REG_TRIGGER, CMD_TRIGGER)) { + ESP_LOGE(TAG, "Failed to trigger measurement!"); + this->status_set_warning(); + return; + } + + // Schedule reading the result after the read delay + this->set_timeout(READ_DELAY, [this]() { this->read_distance_(); }); +} + +void GLR01I2CComponent::read_distance_() { + uint16_t distance = 0; + if (!this->read_byte_16(REG_DISTANCE, &distance)) { + ESP_LOGE(TAG, "Failed to read distance value!"); + this->status_set_warning(); + return; + } + + if (distance == 0xFFFF) { + ESP_LOGW(TAG, "Invalid measurement received!"); + this->status_set_warning(); + } else { + ESP_LOGV(TAG, "Distance: %umm", distance); + this->publish_state(distance); + this->status_clear_warning(); + } +} + +} // namespace gl_r01_i2c +} // namespace esphome diff --git a/esphome/components/gl_r01_i2c/gl_r01_i2c.h b/esphome/components/gl_r01_i2c/gl_r01_i2c.h new file mode 100644 index 0000000000..9a7aa023fd --- /dev/null +++ b/esphome/components/gl_r01_i2c/gl_r01_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace gl_r01_i2c { + +class GLR01I2CComponent : public sensor::Sensor, public i2c::I2CDevice, public PollingComponent { + public: + void setup() override; + void dump_config() override; + void update() override; + + protected: + void read_distance_(); + uint16_t version_{0}; +}; + +} // namespace gl_r01_i2c +} // namespace esphome diff --git a/esphome/components/gl_r01_i2c/sensor.py b/esphome/components/gl_r01_i2c/sensor.py new file mode 100644 index 0000000000..58db72540e --- /dev/null +++ b/esphome/components/gl_r01_i2c/sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_DISTANCE, + STATE_CLASS_MEASUREMENT, + UNIT_MILLIMETER, +) + +CODEOWNERS = ["@pkejval"] +DEPENDENCIES = ["i2c"] + +gl_r01_i2c_ns = cg.esphome_ns.namespace("gl_r01_i2c") +GLR01I2CComponent = gl_r01_i2c_ns.class_( + "GLR01I2CComponent", i2c.I2CDevice, cg.PollingComponent +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + GLR01I2CComponent, + unit_of_measurement=UNIT_MILLIMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x74)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 9f50fd779a..8372bc7e08 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -1,11 +1,22 @@ +import logging + from esphome import pins import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv -from esphome.const import CONF_PIN +from esphome.const import ( + CONF_ALLOW_OTHER_USES, + CONF_ID, + CONF_NAME, + CONF_NUMBER, + CONF_PIN, +) +from esphome.core import CORE from .. import gpio_ns +_LOGGER = logging.getLogger(__name__) + GPIOBinarySensor = gpio_ns.class_( "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component ) @@ -24,7 +35,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 ), @@ -41,6 +66,34 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) - cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT])) - if config[CONF_USE_INTERRUPT]: + # Check for ESP8266 GPIO16 interrupt limitation + # GPIO16 on ESP8266 is a special pin that doesn't support interrupts through + # the Arduino attachInterrupt() function. This is the only known GPIO pin + # across all supported platforms that has this limitation, so we handle it + # here instead of in the platform-specific code. + use_interrupt = config[CONF_USE_INTERRUPT] + if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16: + _LOGGER.warning( + "GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. " + "Falling back to polling mode (same as in ESPHome <2025.7). " + "The sensor will work exactly as before, but other pins have better " + "performance with interrupts.", + config.get(CONF_NAME, config[CONF_ID]), + ) + use_interrupt = False + + # Check if pin is shared with other components (allow_other_uses) + # When a pin is shared, interrupts can interfere with other components + # (e.g., duty_cycle sensor) that need to monitor the pin's state changes + if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False): + _LOGGER.info( + "GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. " + "The sensor will use polling mode for compatibility with other pin uses.", + config.get(CONF_NAME, config[CONF_ID]), + config[CONF_PIN][CONF_NUMBER], + ) + use_interrupt = False + + cg.add(var.set_use_interrupt(use_interrupt)) + if use_interrupt: cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index d3dbcba6ed..2d77f2f7ab 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_HOST, + CoreModel, ) from esphome.core import CORE @@ -43,5 +44,7 @@ async def to_code(config): cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) cg.add_build_flag("-std=gnu++20") cg.add_define("ESPHOME_BOARD", "host") + cg.add_define(CoreModel.MULTI_ATOMICS) cg.add_platformio_option("platform", "platformio/native") cg.add_platformio_option("lib_ldf_mode", "off") + cg.add_platformio_option("lib_compat_mode", "strict") diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 202c7b88b2..06aa6da6a4 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -83,7 +83,7 @@ void HttpRequestUpdate::update_task(void *params) { container.reset(); // Release ownership of the container's shared_ptr valid = json::parse_json(response, [this_update](JsonObject root) -> bool { - if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { + if (!root["name"].is() || !root["version"].is() || !root["builds"].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } @@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) { this_update->update_info_.latest_version = root["version"].as(); for (auto build : root["builds"].as()) { - if (!build.containsKey("chipFamily")) { + if (!build["chipFamily"].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } if (build["chipFamily"] == ESPHOME_VARIANT) { - if (!build.containsKey("ota")) { + if (!build["ota"].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - auto ota = build["ota"]; - if (!ota.containsKey("path") || !ota.containsKey("md5")) { + JsonObject ota = build["ota"].as(); + if (!ota["path"].is() || !ota["md5"].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } this_update->update_info_.firmware_url = ota["path"].as(); this_update->update_info_.md5 = ota["md5"].as(); - if (ota.containsKey("summary")) + if (ota["summary"].is()) this_update->update_info_.summary = ota["summary"].as(); - if (ota.containsKey("release_url")) + if (ota["release_url"].is()) this_update->update_info_.release_url = ota["release_url"].as(); return 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/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index bb9f24bf0b..cb7b876a40 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -180,7 +180,7 @@ async def to_code(config): await speaker.register_speaker(var, config) if config[CONF_DAC_TYPE] == "internal": - cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL])) + cg.add(var.set_internal_dac_mode(config[CONF_MODE])) else: cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) if use_legacy(): diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index 6a0e4c50d2..9773bf67ce 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All( @coroutine_with_priority(1.0) async def to_code(config): - cg.add_library("bblanchon/ArduinoJson", "6.18.5") + cg.add_library("bblanchon/ArduinoJson", "7.4.2") cg.add_define("USE_JSON") cg.add_global(json_ns.using) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 6c66476dc1..94c531222a 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -1,83 +1,76 @@ #include "json_util.h" #include "esphome/core/log.h" +// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h + namespace esphome { namespace json { static const char *const TAG = "json"; -static std::vector global_json_build_buffer; // NOLINT -static const auto ALLOCATOR = RAMAllocator(RAMAllocator::ALLOC_INTERNAL); +// Build an allocator for the JSON Library using the RAMAllocator class +struct SpiRamAllocator : ArduinoJson::Allocator { + void *allocate(size_t size) override { return this->allocator_.allocate(size); } + + void deallocate(void *pointer) override { + // ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate. + // RAMAllocator::deallocate() requires the size, which we don't have access to here. + // RAMAllocator::deallocate implementation just calls free() regardless of whether + // the memory was allocated with heap_caps_malloc or malloc. + // This is safe because ESP-IDF's heap implementation internally tracks the memory region + // and routes free() to the appropriate heap. + free(pointer); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) + } + + void *reallocate(void *ptr, size_t new_size) override { + return this->allocator_.reallocate(static_cast(ptr), new_size); + } + + protected: + RAMAllocator allocator_{RAMAllocator(RAMAllocator::NONE)}; +}; std::string build_json(const json_build_t &f) { - // Here we are allocating up to 5kb of memory, - // with the heap size minus 2kb to be safe if less than 5kb - // as we can not have a true dynamic sized document. - // The excess memory is freed below with `shrinkToFit()` - auto free_heap = ALLOCATOR.get_max_free_block_size(); - size_t request_size = std::min(free_heap, (size_t) 512); - while (true) { - ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size); - DynamicJsonDocument json_document(request_size); - if (json_document.capacity() == 0) { - ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, largest free heap block: %zu bytes", - request_size, free_heap); - return "{}"; - } - JsonObject root = json_document.to(); - f(root); - if (json_document.overflowed()) { - if (request_size == free_heap) { - ESP_LOGE(TAG, "Could not allocate memory for document! Overflowed largest free heap block: %zu bytes", - free_heap); - return "{}"; - } - request_size = std::min(request_size * 2, free_heap); - continue; - } - json_document.shrinkToFit(); - ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity()); - std::string output; - serializeJson(json_document, output); - return output; + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + auto doc_allocator = SpiRamAllocator(); + JsonDocument json_document(&doc_allocator); + if (json_document.overflowed()) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); + return "{}"; } + JsonObject root = json_document.to(); + f(root); + if (json_document.overflowed()) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); + return "{}"; + } + std::string output; + serializeJson(json_document, output); + return output; + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } bool parse_json(const std::string &data, const json_parse_t &f) { - // Here we are allocating 1.5 times the data size, - // with the heap size minus 2kb to be safe if less than that - // as we can not have a true dynamic sized document. - // The excess memory is freed below with `shrinkToFit()` - auto free_heap = ALLOCATOR.get_max_free_block_size(); - size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5)); - while (true) { - DynamicJsonDocument json_document(request_size); - if (json_document.capacity() == 0) { - ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, free heap: %zu", request_size, - free_heap); - return false; - } - DeserializationError err = deserializeJson(json_document, data); - json_document.shrinkToFit(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + auto doc_allocator = SpiRamAllocator(); + JsonDocument json_document(&doc_allocator); + if (json_document.overflowed()) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); + return false; + } + DeserializationError err = deserializeJson(json_document, data); - JsonObject root = json_document.as(); + JsonObject root = json_document.as(); - if (err == DeserializationError::Ok) { - return f(root); - } else if (err == DeserializationError::NoMemory) { - if (request_size * 2 >= free_heap) { - ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); - return false; - } - ESP_LOGV(TAG, "Increasing memory allocation."); - request_size *= 2; - continue; - } else { - ESP_LOGE(TAG, "Parse error: %s", err.c_str()); - return false; - } - }; + if (err == DeserializationError::Ok) { + return f(root); + } else if (err == DeserializationError::NoMemory) { + ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); + return false; + } + ESP_LOGE(TAG, "Parse error: %s", err.c_str()); return false; + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } } // namespace json diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 375d1088e8..8f1fb0ee9d 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -178,13 +178,8 @@ static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } -static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { - for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) { - if (header_footer[i] != buffer[i]) { - return false; // Mismatch in header/footer - } - } - return true; // Valid header/footer +static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { + return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; } void LD2410Component::dump_config() { @@ -300,14 +295,12 @@ void LD2410Component::send_command_(uint8_t command, const uint8_t *command_valu if (command_value != nullptr) { len += command_value_len; } - uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00}; + // 2 length bytes (low, high) + 2 command bytes (low, high) + uint8_t len_cmd[] = {len, 0x00, command, 0x00}; this->write_array(len_cmd, sizeof(len_cmd)); - // command value bytes if (command_value != nullptr) { - for (uint8_t i = 0; i < command_value_len; i++) { - this->write_byte(command_value[i]); - } + this->write_array(command_value, command_value_len); } // frame footer bytes this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)); @@ -401,7 +394,7 @@ void LD2410Component::handle_periodic_data_() { /* Moving distance range: 18th byte Still distance range: 19th byte - Moving enery: 20~28th bytes + Moving energy: 20~28th bytes */ for (std::vector::size_type i = 0; i != this->gate_move_sensors_.size(); i++) { sensor::Sensor *s = this->gate_move_sensors_[i]; @@ -480,7 +473,7 @@ bool LD2410Component::handle_ack_data_() { ESP_LOGE(TAG, "Invalid status"); return true; } - if (ld2410::two_byte_to_int(this->buffer_data_[8], this->buffer_data_[9]) != 0x00) { + if (this->buffer_data_[8] || this->buffer_data_[9]) { ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]); return true; } @@ -534,8 +527,8 @@ bool LD2410Component::handle_ack_data_() { const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_); ESP_LOGV(TAG, - "Light function is: %s\n" - "Light threshold is: %u\n" + "Light function: %s\n" + "Light threshold: %u\n" "Out pin level: %s", light_function_str, this->light_threshold_, out_pin_level_str); #ifdef USE_SELECT @@ -600,7 +593,7 @@ bool LD2410Component::handle_ack_data_() { break; case CMD_QUERY: { // Query parameters response - if (this->buffer_data_[10] != 0xAA) + if (this->buffer_data_[10] != HEADER) return true; // value head=0xAA #ifdef USE_NUMBER /* @@ -656,17 +649,11 @@ void LD2410Component::readline_(int readch) { if (this->buffer_pos_ < 4) { return; // Not enough data to process yet } - if (this->buffer_data_[this->buffer_pos_ - 4] == DATA_FRAME_FOOTER[0] && - this->buffer_data_[this->buffer_pos_ - 3] == DATA_FRAME_FOOTER[1] && - this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[2] && - this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[3]) { + if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); this->handle_periodic_data_(); this->buffer_pos_ = 0; // Reset position index for next message - } else if (this->buffer_data_[this->buffer_pos_ - 4] == CMD_FRAME_FOOTER[0] && - this->buffer_data_[this->buffer_pos_ - 3] == CMD_FRAME_FOOTER[1] && - this->buffer_data_[this->buffer_pos_ - 2] == CMD_FRAME_FOOTER[2] && - this->buffer_data_[this->buffer_pos_ - 1] == CMD_FRAME_FOOTER[3]) { + } else if (ld2410::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); if (this->handle_ack_data_()) { this->buffer_pos_ = 0; // Reset position index for next message @@ -772,7 +759,6 @@ void LD2410Component::set_max_distances_timeout() { 0x00}; this->set_config_mode_(true); this->send_command_(CMD_MAXDIST_DURATION, value, sizeof(value)); - delay(50); // NOLINT this->query_parameters_(); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); this->set_config_mode_(false); @@ -802,7 +788,6 @@ void LD2410Component::set_gate_threshold(uint8_t gate) { 0x01, 0x00, lowbyte(motion), highbyte(motion), 0x00, 0x00, 0x02, 0x00, lowbyte(still), highbyte(still), 0x00, 0x00}; this->send_command_(CMD_GATE_SENS, value, sizeof(value)); - delay(50); // NOLINT this->query_parameters_(); this->set_config_mode_(false); } @@ -833,7 +818,6 @@ void LD2410Component::set_light_out_control() { this->set_config_mode_(true); uint8_t value[4] = {this->light_function_, this->light_threshold_, this->out_pin_level_, 0x00}; this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value)); - delay(50); // NOLINT this->query_light_control_(); this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); this->set_config_mode_(false); diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp index c6ea0a348b..d8632e9c19 100644 --- a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp @@ -5,10 +5,10 @@ namespace esphome { namespace ld2420 { -static const char *const TAG = "LD2420.binary_sensor"; +static const char *const TAG = "ld2420.binary_sensor"; void LD2420BinarySensor::dump_config() { - ESP_LOGCONFIG(TAG, "LD2420 BinarySensor:"); + ESP_LOGCONFIG(TAG, "Binary Sensor:"); LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_); } diff --git a/esphome/components/ld2420/button/reconfig_buttons.cpp b/esphome/components/ld2420/button/reconfig_buttons.cpp index 3537c1d64a..fb8ec2b5a6 100644 --- a/esphome/components/ld2420/button/reconfig_buttons.cpp +++ b/esphome/components/ld2420/button/reconfig_buttons.cpp @@ -2,7 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -static const char *const TAG = "LD2420.button"; +static const char *const TAG = "ld2420.button"; namespace esphome { namespace ld2420 { diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 8a7d7de23b..0baff368c8 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -137,7 +137,7 @@ static const std::string OP_SIMPLE_MODE_STRING = "Simple"; // Memory-efficient lookup tables struct StringToUint8 { const char *str; - uint8_t value; + const uint8_t value; }; static constexpr StringToUint8 OP_MODE_BY_STR[] = { @@ -155,8 +155,9 @@ static constexpr const char *ERR_MESSAGE[] = { // Helper function for lookups template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { for (const auto &entry : arr) { - if (str == entry.str) + if (str == entry.str) { return entry.value; + } } return 0xFF; // Not found } @@ -326,15 +327,8 @@ void LD2420Component::revert_config_action() { void LD2420Component::loop() { // If there is a active send command do not process it here, the send command call will handle it. - if (!this->get_cmd_active_()) { - if (!this->available()) - return; - static uint8_t buffer[2048]; - static uint8_t rx_data; - while (this->available()) { - rx_data = this->read(); - this->readline_(rx_data, buffer, sizeof(buffer)); - } + while (!this->cmd_active_ && this->available()) { + this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH); } } @@ -365,8 +359,9 @@ void LD2420Component::auto_calibrate_sensitivity() { // Store average and peak values this->gate_avg[gate] = sum / CALIBRATE_SAMPLES; - if (this->gate_peak[gate] < peak) + if (this->gate_peak[gate] < peak) { this->gate_peak[gate] = peak; + } uint32_t calculated_value = (static_cast(this->gate_peak[gate]) + (move_factor * static_cast(this->gate_peak[gate]))); @@ -403,8 +398,9 @@ void LD2420Component::set_operating_mode(const std::string &state) { } } else { // Set the current data back so we don't have new data that can be applied in error. - if (this->get_calibration_()) + if (this->get_calibration_()) { memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); + } this->set_calibration_(false); } } else { @@ -414,30 +410,32 @@ void LD2420Component::set_operating_mode(const std::string &state) { } void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) { - static int pos = 0; - - if (rx_data >= 0) { - if (pos < len - 1) { - buffer[pos++] = rx_data; - buffer[pos] = 0; - } else { - pos = 0; - } - if (pos >= 4) { - if (memcmp(&buffer[pos - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) { - this->set_cmd_active_(false); // Set command state to inactive after responce. - this->handle_ack_data_(buffer, pos); - pos = 0; - } else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) && - (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { - this->handle_simple_mode_(buffer, pos); - pos = 0; - } else if ((memcmp(&buffer[pos - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) && - (this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { - this->handle_energy_mode_(buffer, pos); - pos = 0; - } - } + if (rx_data < 0) { + return; // No data available + } + if (this->buffer_pos_ < len - 1) { + buffer[this->buffer_pos_++] = rx_data; + buffer[this->buffer_pos_] = 0; + } else { + // We should never get here, but just in case... + ESP_LOGW(TAG, "Max command length exceeded; ignoring"); + this->buffer_pos_ = 0; + } + if (this->buffer_pos_ < 4) { + return; // Not enough data to process yet + } + if (memcmp(&buffer[this->buffer_pos_ - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) { + this->cmd_active_ = false; // Set command state to inactive after response + this->handle_ack_data_(buffer, this->buffer_pos_); + this->buffer_pos_ = 0; + } else if ((buffer[this->buffer_pos_ - 2] == 0x0D && buffer[this->buffer_pos_ - 1] == 0x0A) && + (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { + this->handle_simple_mode_(buffer, this->buffer_pos_); + this->buffer_pos_ = 0; + } else if ((memcmp(&buffer[this->buffer_pos_ - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) && + (this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { + this->handle_energy_mode_(buffer, this->buffer_pos_); + this->buffer_pos_ = 0; } } @@ -462,8 +460,9 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { // Resonable refresh rate for home assistant database size health const int32_t current_millis = App.get_loop_component_start_time(); - if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) + if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) { return; + } this->last_periodic_millis = current_millis; for (auto &listener : this->listeners_) { listener->on_distance(this->get_distance_()); @@ -506,14 +505,16 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { } } outbuf[index] = '\0'; - if (index > 1) + if (index > 1) { this->set_distance_(strtol(outbuf, &endptr, 10)); + } if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { // Resonable refresh rate for home assistant database size health const int32_t current_millis = App.get_loop_component_start_time(); - if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) + if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) { return; + } this->last_normal_periodic_millis = current_millis; for (auto &listener : this->listeners_) listener->on_distance(this->get_distance_()); @@ -593,11 +594,12 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { int LD2420Component::send_cmd_from_array(CmdFrameT frame) { uint32_t start_millis = millis(); uint8_t error = 0; - uint8_t ack_buffer[64]; - uint8_t cmd_buffer[64]; + uint8_t ack_buffer[MAX_LINE_LENGTH]; + uint8_t cmd_buffer[MAX_LINE_LENGTH]; this->cmd_reply_.ack = false; - if (frame.command != CMD_RESTART) - this->set_cmd_active_(true); // Restart does not reply, thus no ack state required. + if (frame.command != CMD_RESTART) { + this->cmd_active_ = true; + } // Restart does not reply, thus no ack state required uint8_t retry = 3; while (retry) { frame.length = 0; @@ -619,9 +621,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer)); frame.length += sizeof(frame.footer); - for (uint16_t index = 0; index < frame.length; index++) { - this->write_byte(cmd_buffer[index]); - } + this->write_array(cmd_buffer, frame.length); error = 0; if (frame.command == CMD_RESTART) { @@ -630,7 +630,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { while (!this->cmd_reply_.ack) { while (this->available()) { - this->readline_(read(), ack_buffer, sizeof(ack_buffer)); + this->readline_(this->read(), ack_buffer, sizeof(ack_buffer)); } delay_microseconds_safe(1450); // Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT. @@ -641,10 +641,12 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { break; } } - if (this->cmd_reply_.ack) + if (this->cmd_reply_.ack) { retry = 0; - if (this->cmd_reply_.error > 0) + } + if (this->cmd_reply_.error > 0) { this->handle_cmd_error(error); + } } return error; } @@ -764,8 +766,9 @@ void LD2420Component::set_system_mode(uint16_t mode) { cmd_frame.data_length += sizeof(unknown_parm); cmd_frame.footer = CMD_FRAME_FOOTER; ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command); - if (this->send_cmd_from_array(cmd_frame) == 0) + if (this->send_cmd_from_array(cmd_frame) == 0) { this->set_mode_(mode); + } } void LD2420Component::get_firmware_version_() { @@ -840,18 +843,24 @@ void LD2420Component::set_gate_threshold(uint8_t gate) { #ifdef USE_NUMBER void LD2420Component::init_gate_config_numbers() { - if (this->gate_timeout_number_ != nullptr) + if (this->gate_timeout_number_ != nullptr) { this->gate_timeout_number_->publish_state(static_cast(this->current_config.timeout)); - if (this->gate_select_number_ != nullptr) + } + if (this->gate_select_number_ != nullptr) { this->gate_select_number_->publish_state(0); - if (this->min_gate_distance_number_ != nullptr) + } + if (this->min_gate_distance_number_ != nullptr) { this->min_gate_distance_number_->publish_state(static_cast(this->current_config.min_gate)); - if (this->max_gate_distance_number_ != nullptr) + } + if (this->max_gate_distance_number_ != nullptr) { this->max_gate_distance_number_->publish_state(static_cast(this->current_config.max_gate)); - if (this->gate_move_sensitivity_factor_number_ != nullptr) + } + if (this->gate_move_sensitivity_factor_number_ != nullptr) { this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor); - if (this->gate_still_sensitivity_factor_number_ != nullptr) + } + if (this->gate_still_sensitivity_factor_number_ != nullptr) { this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor); + } for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { if (this->gate_still_threshold_numbers_[gate] != nullptr) { this->gate_still_threshold_numbers_[gate]->publish_state( diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index d574a25c89..812c408cfd 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -20,8 +20,9 @@ namespace esphome { namespace ld2420 { -static const uint8_t TOTAL_GATES = 16; static const uint8_t CALIBRATE_SAMPLES = 64; +static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +static const uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { OP_NORMAL_MODE = 1, @@ -118,10 +119,10 @@ class LD2420Component : public Component, public uart::UARTDevice { float gate_move_sensitivity_factor{0.5}; float gate_still_sensitivity_factor{0.5}; - int32_t last_periodic_millis = millis(); - int32_t report_periodic_millis = millis(); - int32_t monitor_periodic_millis = millis(); - int32_t last_normal_periodic_millis = millis(); + int32_t last_periodic_millis{0}; + int32_t report_periodic_millis{0}; + int32_t monitor_periodic_millis{0}; + int32_t last_normal_periodic_millis{0}; uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES]; uint16_t gate_avg[TOTAL_GATES]; uint16_t gate_peak[TOTAL_GATES]; @@ -161,8 +162,6 @@ class LD2420Component : public Component, public uart::UARTDevice { void set_presence_(bool presence) { this->presence_ = presence; }; uint16_t get_distance_() { return this->distance_; }; void set_distance_(uint16_t distance) { this->distance_ = distance; }; - bool get_cmd_active_() { return this->cmd_active_; }; - void set_cmd_active_(bool active) { this->cmd_active_ = active; }; void handle_simple_mode_(const uint8_t *inbuf, int len); void handle_energy_mode_(uint8_t *buffer, int len); void handle_ack_data_(uint8_t *buffer, int len); @@ -181,12 +180,11 @@ class LD2420Component : public Component, public uart::UARTDevice { std::vector gate_move_threshold_numbers_ = std::vector(16); #endif - uint32_t max_distance_gate_; - uint32_t min_distance_gate_; + uint16_t distance_{0}; uint16_t system_mode_; uint16_t gate_energy_[TOTAL_GATES]; - uint16_t distance_{0}; - uint8_t config_checksum_{0}; + uint8_t buffer_pos_{0}; // where to resume processing/populating buffer + uint8_t buffer_data_[MAX_LINE_LENGTH]; char firmware_ver_[8]{"v0.0.0"}; bool cmd_active_{false}; bool presence_{false}; diff --git a/esphome/components/ld2420/number/gate_config_number.cpp b/esphome/components/ld2420/number/gate_config_number.cpp index e5eaafb46d..a373753770 100644 --- a/esphome/components/ld2420/number/gate_config_number.cpp +++ b/esphome/components/ld2420/number/gate_config_number.cpp @@ -2,7 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -static const char *const TAG = "LD2420.number"; +static const char *const TAG = "ld2420.number"; namespace esphome { namespace ld2420 { diff --git a/esphome/components/ld2420/select/operating_mode_select.cpp b/esphome/components/ld2420/select/operating_mode_select.cpp index 1c59f443a5..2d576e7cc6 100644 --- a/esphome/components/ld2420/select/operating_mode_select.cpp +++ b/esphome/components/ld2420/select/operating_mode_select.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace ld2420 { -static const char *const TAG = "LD2420.select"; +static const char *const TAG = "ld2420.select"; void LD2420Select::control(const std::string &value) { this->publish_state(value); diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.cpp b/esphome/components/ld2420/sensor/ld2420_sensor.cpp index 97f0c594b7..723604f396 100644 --- a/esphome/components/ld2420/sensor/ld2420_sensor.cpp +++ b/esphome/components/ld2420/sensor/ld2420_sensor.cpp @@ -5,10 +5,10 @@ namespace esphome { namespace ld2420 { -static const char *const TAG = "LD2420.sensor"; +static const char *const TAG = "ld2420.sensor"; void LD2420Sensor::dump_config() { - ESP_LOGCONFIG(TAG, "LD2420 Sensor:"); + ESP_LOGCONFIG(TAG, "Sensor:"); LOG_SENSOR(" ", "Distance", this->distance_sensor_); } diff --git a/esphome/components/ld2420/text_sensor/text_sensor.cpp b/esphome/components/ld2420/text_sensor/text_sensor.cpp index 1dcdcf7d60..73af3b3660 100644 --- a/esphome/components/ld2420/text_sensor/text_sensor.cpp +++ b/esphome/components/ld2420/text_sensor/text_sensor.cpp @@ -5,10 +5,10 @@ namespace esphome { namespace ld2420 { -static const char *const TAG = "LD2420.text_sensor"; +static const char *const TAG = "ld2420.text_sensor"; void LD2420TextSensor::dump_config() { - ESP_LOGCONFIG(TAG, "LD2420 TextSensor:"); + ESP_LOGCONFIG(TAG, "Text Sensor:"); LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_); } diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 149e5d1179..7f2a0bc0a5 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -20,6 +20,7 @@ from esphome.const import ( KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, + CoreModel, __version__, ) from esphome.core import CORE @@ -260,6 +261,7 @@ async def component_to_code(config): cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]]) + cg.add_define(CoreModel.MULTI_NO_ATOMICS) # force using arduino framework cg.add_platformio_option("framework", "arduino") @@ -268,6 +270,7 @@ async def component_to_code(config): # disable library compatibility checks cg.add_platformio_option("lib_ldf_mode", "off") + cg.add_platformio_option("lib_compat_mode", "soft") # include in every file cg.add_platformio_option("build_src_flags", "-include Arduino.h") # dummy version code 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/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 6f8cc11f25..26615bae5c 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -9,6 +9,7 @@ namespace light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema void LightJSONSchema::dump_json(LightState &state, JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson if (state.supports_effects()) root["effect"] = state.get_effect_name(); @@ -52,7 +53,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) { if (values.get_color_mode() & ColorCapability::BRIGHTNESS) root["brightness"] = uint8_t(values.get_brightness() * 255); - JsonObject color = root.createNestedObject("color"); + JsonObject color = root["color"].to(); if (values.get_color_mode() & ColorCapability::RGB) { color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); @@ -73,7 +74,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) { } void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) { - if (root.containsKey("state")) { + if (root["state"].is()) { auto val = parse_on_off(root["state"]); switch (val) { case PARSE_ON: @@ -90,40 +91,40 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO } } - if (root.containsKey("brightness")) { + if (root["brightness"].is()) { call.set_brightness(float(root["brightness"]) / 255.0f); } - if (root.containsKey("color")) { + if (root["color"].is()) { JsonObject color = root["color"]; // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. float max_rgb = 0.0f; - if (color.containsKey("r")) { + if (color["r"].is()) { float r = float(color["r"]) / 255.0f; max_rgb = fmaxf(max_rgb, r); call.set_red(r); } - if (color.containsKey("g")) { + if (color["g"].is()) { float g = float(color["g"]) / 255.0f; max_rgb = fmaxf(max_rgb, g); call.set_green(g); } - if (color.containsKey("b")) { + if (color["b"].is()) { float b = float(color["b"]) / 255.0f; max_rgb = fmaxf(max_rgb, b); call.set_blue(b); } - if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { + if (color["r"].is() || color["g"].is() || color["b"].is()) { call.set_color_brightness(max_rgb); } - if (color.containsKey("c")) { + if (color["c"].is()) { call.set_cold_white(float(color["c"]) / 255.0f); } - if (color.containsKey("w")) { + if (color["w"].is()) { // the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm // white channel in RGBWW. - if (color.containsKey("c")) { + if (color["c"].is()) { call.set_warm_white(float(color["w"]) / 255.0f); } else { call.set_white(float(color["w"]) / 255.0f); @@ -131,11 +132,11 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO } } - if (root.containsKey("white_value")) { // legacy API + if (root["white_value"].is()) { // legacy API call.set_white(float(root["white_value"]) / 255.0f); } - if (root.containsKey("color_temp")) { + if (root["color_temp"].is()) { call.set_color_temperature(float(root["color_temp"])); } } @@ -143,17 +144,17 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) { LightJSONSchema::parse_color_json(state, call, root); - if (root.containsKey("flash")) { + if (root["flash"].is()) { auto length = uint32_t(float(root["flash"]) * 1000); call.set_flash_length(length); } - if (root.containsKey("transition")) { + if (root["transition"].is()) { auto length = uint32_t(float(root["transition"]) * 1000); call.set_transition_length(length); } - if (root.containsKey("effect")) { + if (root["effect"].is()) { const char *effect = root["effect"]; call.set_effect(effect); } 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/lps22/__init__.py b/esphome/components/lps22/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/lps22/lps22.cpp b/esphome/components/lps22/lps22.cpp new file mode 100644 index 0000000000..526286ba72 --- /dev/null +++ b/esphome/components/lps22/lps22.cpp @@ -0,0 +1,75 @@ +#include "lps22.h" + +namespace esphome { +namespace lps22 { + +static constexpr const char *const TAG = "lps22"; + +static constexpr uint8_t WHO_AM_I = 0x0F; +static constexpr uint8_t LPS22HB_ID = 0xB1; +static constexpr uint8_t LPS22HH_ID = 0xB3; +static constexpr uint8_t CTRL_REG2 = 0x11; +static constexpr uint8_t CTRL_REG2_ONE_SHOT_MASK = 0b1; +static constexpr uint8_t STATUS = 0x27; +static constexpr uint8_t STATUS_T_DA_MASK = 0b10; +static constexpr uint8_t STATUS_P_DA_MASK = 0b01; +static constexpr uint8_t TEMP_L = 0x2b; +static constexpr uint8_t PRES_OUT_XL = 0x28; +static constexpr uint8_t REF_P_XL = 0x28; +static constexpr uint8_t READ_ATTEMPTS = 10; +static constexpr uint8_t READ_INTERVAL = 5; +static constexpr float PRESSURE_SCALE = 1.0f / 4096.0f; +static constexpr float TEMPERATURE_SCALE = 0.01f; + +void LPS22Component::setup() { + uint8_t value = 0x00; + this->read_register(WHO_AM_I, &value, 1); + if (value != LPS22HB_ID && value != LPS22HH_ID) { + ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB or LPS22HH ID", value); + this->mark_failed(); + } +} + +void LPS22Component::dump_config() { + ESP_LOGCONFIG(TAG, "LPS22:"); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); +} + +void LPS22Component::update() { + uint8_t value = 0x00; + this->read_register(CTRL_REG2, &value, 1); + value |= CTRL_REG2_ONE_SHOT_MASK; + this->write_register(CTRL_REG2, &value, 1); + this->set_retry(READ_INTERVAL, READ_ATTEMPTS, [this](uint8_t _) { return this->try_read_(); }); +} + +RetryResult LPS22Component::try_read_() { + uint8_t value = 0x00; + this->read_register(STATUS, &value, 1); + const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK; + if ((value & expected_status_mask) != expected_status_mask) { + ESP_LOGD(TAG, "STATUS not ready: %x", value); + return RetryResult::RETRY; + } + + if (this->temperature_sensor_ != nullptr) { + uint8_t t_buf[2]{0}; + this->read_register(TEMP_L, t_buf, 2); + int16_t encoded = static_cast(encode_uint16(t_buf[1], t_buf[0])); + float temp = TEMPERATURE_SCALE * static_cast(encoded); + this->temperature_sensor_->publish_state(temp); + } + if (this->pressure_sensor_ != nullptr) { + uint8_t p_buf[3]{0}; + this->read_register(PRES_OUT_XL, p_buf, 3); + uint32_t p_lsb = encode_uint24(p_buf[2], p_buf[1], p_buf[0]); + this->pressure_sensor_->publish_state(PRESSURE_SCALE * static_cast(p_lsb)); + } + return RetryResult::DONE; +} + +} // namespace lps22 +} // namespace esphome diff --git a/esphome/components/lps22/lps22.h b/esphome/components/lps22/lps22.h new file mode 100644 index 0000000000..549ea524ea --- /dev/null +++ b/esphome/components/lps22/lps22.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace lps22 { + +class LPS22Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; } + + void setup() override; + void update() override; + void dump_config() override; + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + + RetryResult try_read_(); +}; + +} // namespace lps22 +} // namespace esphome diff --git a/esphome/components/lps22/sensor.py b/esphome/components/lps22/sensor.py new file mode 100644 index 0000000000..08e97ee7b7 --- /dev/null +++ b/esphome/components/lps22/sensor.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + ICON_THERMOMETER, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, +) + +CODEOWNERS = ["@nagisa"] +DEPENDENCIES = ["i2c"] + +lps22 = cg.esphome_ns.namespace("lps22") + +LPS22Component = lps22.class_("LPS22Component", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LPS22Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x5D)) # can also be 0x5C +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) 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 840511da69..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 ( @@ -29,9 +30,9 @@ from ..defines import ( ) from ..helpers import add_lv_use, lvgl_components_required from ..lv_validation import ( - angle, get_end_value, get_start_value, + lv_angle, lv_bool, lv_color, lv_float, @@ -162,7 +163,7 @@ SCALE_SCHEMA = cv.Schema( cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), - cv.Optional(CONF_ROTATION): angle, + cv.Optional(CONF_ROTATION): lv_angle, cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), } ) @@ -187,7 +188,9 @@ class MeterType(WidgetType): for scale_conf in config.get(CONF_SCALES, ()): rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 if CONF_ROTATION in scale_conf: - rotation = scale_conf[CONF_ROTATION] // 10 + 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: @@ -205,21 +208,20 @@ class MeterType(WidgetType): var, meter_var, ticks[CONF_COUNT], - ticks[CONF_WIDTH], - ticks[CONF_LENGTH], + await size.process(ticks[CONF_WIDTH]), + await size.process(ticks[CONF_LENGTH]), color, ) if CONF_MAJOR in ticks: major = ticks[CONF_MAJOR] - color = await lv_color.process(major[CONF_COLOR]) lv.meter_set_scale_major_ticks( var, meter_var, major[CONF_STRIDE], - major[CONF_WIDTH], - major[CONF_LENGTH], - color, - major[CONF_LABEL_GAP], + await size.process(major[CONF_WIDTH]), + await size.process(major[CONF_LENGTH]), + await lv_color.process(major[CONF_COLOR]), + await size.process(major[CONF_LABEL_GAP]), ) for indicator in scale_conf.get(CONF_INDICATORS, ()): (t, v) = next(iter(indicator.items())) @@ -233,7 +235,11 @@ class MeterType(WidgetType): lv_assign( ivar, lv_expr.meter_add_needle_line( - var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] + var, + meter_var, + await size.process(v[CONF_WIDTH]), + color, + await size.process(v[CONF_R_MOD]), ), ) if t == CONF_ARC: @@ -241,7 +247,11 @@ class MeterType(WidgetType): lv_assign( ivar, lv_expr.meter_add_arc( - var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] + var, + meter_var, + await size.process(v[CONF_WIDTH]), + color, + await size.process(v[CONF_R_MOD]), ), ) if t == CONF_TICK_STYLE: @@ -257,7 +267,7 @@ class MeterType(WidgetType): color_start, color_end, v[CONF_LOCAL], - 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/max7219digit/display.py b/esphome/components/max7219digit/display.py index f195078c1a..fef121ff10 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import display, spi import esphome.config_validation as cv from esphome.const import ( + CONF_FLIP_X, CONF_ID, CONF_INTENSITY, CONF_LAMBDA, @@ -14,7 +15,6 @@ CODEOWNERS = ["@rspaargaren"] DEPENDENCIES = ["spi"] CONF_ROTATE_CHIP = "rotate_chip" -CONF_FLIP_X = "flip_x" CONF_SCROLL_SPEED = "scroll_speed" CONF_SCROLL_DWELL = "scroll_dwell" CONF_SCROLL_DELAY = "scroll_delay" 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..00b861f71b 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,526 @@ 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 { + if (!this->get_clipping().inside(x, y)) + return; + 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_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 0a38598679..94460c31a7 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -55,7 +55,8 @@ void MQTTAlarmControlPanelComponent::dump_config() { } void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + JsonArray supported_features = root[MQTT_SUPPORTED_FEATURES].to(); const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features(); if (acp_supported_features & ACP_FEAT_ARM_AWAY) { supported_features.add("arm_away"); diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index a096408aa5..623206a0cd 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -153,11 +153,15 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { case MQTT_EVENT_DATA: { static std::string topic; if (!event.topic.empty()) { + // When a single message arrives as multiple chunks, the topic will be empty + // on any but the first message, leading to event.topic being an empty string. + // To ensure handlers get the correct topic, cache the last seen topic to + // simulate always receiving the topic from underlying library topic = event.topic; } ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str()); - this->on_message_.call(!event.topic.empty() ? topic.c_str() : nullptr, event.data.data(), event.data.size(), - event.current_data_offset, event.total_data_len); + this->on_message_.call(topic.c_str(), event.data.data(), event.data.size(), event.current_data_offset, + event.total_data_len); } break; case MQTT_EVENT_ERROR: ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 6d12e88391..2ce4928574 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -30,6 +30,7 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor } void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson if (!this->binary_sensor_->get_device_class().empty()) root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); if (this->binary_sensor_->is_status_binary_sensor()) diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index 204f60fe67..c619a02344 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -31,9 +31,12 @@ void MQTTButtonComponent::dump_config() { } void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson config.state_topic = false; - if (!this->button_->get_device_class().empty()) + if (!this->button_->get_device_class().empty()) { root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } std::string MQTTButtonComponent::component_type() const { return "button"; } diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index ab7fd15a35..f3e57a66be 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -92,6 +92,7 @@ void MQTTClientComponent::send_device_info_() { std::string topic = "esphome/discover/"; topic.append(App.get_name()); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson this->publish_json( topic, [](JsonObject root) { @@ -147,6 +148,7 @@ void MQTTClientComponent::send_device_info_() { #endif }, 2, this->discovery_info_.retain); + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } void MQTTClientComponent::dump_config() { @@ -191,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_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index a8768114a4..e16f097812 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -14,6 +14,7 @@ static const char *const TAG = "mqtt.climate"; using namespace esphome::climate; void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson auto traits = this->device_->get_traits(); // current_temperature_topic if (traits.get_supports_current_temperature()) { @@ -28,7 +29,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // mode_state_topic root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic(); // modes - JsonArray modes = root.createNestedArray(MQTT_MODES); + JsonArray modes = root[MQTT_MODES].to(); // sort array for nice UI in HA if (traits.supports_mode(CLIMATE_MODE_AUTO)) modes.add("auto"); @@ -89,7 +90,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // preset_mode_state_topic root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic(); // presets - JsonArray presets = root.createNestedArray("preset_modes"); + JsonArray presets = root["preset_modes"].to(); if (traits.supports_preset(CLIMATE_PRESET_HOME)) presets.add("home"); if (traits.supports_preset(CLIMATE_PRESET_AWAY)) @@ -119,7 +120,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // fan_mode_state_topic root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); // fan_modes - JsonArray fan_modes = root.createNestedArray("fan_modes"); + JsonArray fan_modes = root["fan_modes"].to(); if (traits.supports_fan_mode(CLIMATE_FAN_ON)) fan_modes.add("on"); if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) @@ -150,7 +151,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // swing_mode_state_topic root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); // swing_modes - JsonArray swing_modes = root.createNestedArray("swing_modes"); + JsonArray swing_modes = root["swing_modes"].to(); if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) swing_modes.add("off"); if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) @@ -163,6 +164,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo config.state_topic = false; config.command_topic = false; + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } void MQTTClimateComponent::setup() { auto traits = this->device_->get_traits(); diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index eee5644c9d..6ceaf219ff 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -70,6 +70,7 @@ bool MQTTComponent::send_discovery_() { ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str()); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return global_mqtt_client->publish_json( this->get_discovery_topic_(discovery_info), [this](JsonObject root) { @@ -128,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(); @@ -155,7 +151,7 @@ bool MQTTComponent::send_discovery_() { } std::string node_area = App.get_area(); - JsonObject device_info = root.createNestedObject(MQTT_DEVICE); + JsonObject device_info = root[MQTT_DEVICE].to(); const auto mac = get_mac_address(); device_info[MQTT_DEVICE_IDENTIFIERS] = mac; device_info[MQTT_DEVICE_NAME] = node_friendly_name; @@ -192,6 +188,7 @@ bool MQTTComponent::send_discovery_() { device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac; }, this->qos_, discovery_info.retain); + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } uint8_t MQTTComponent::get_qos() const { return this->qos_; } @@ -284,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_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 8d09d836f3..6fb61ee469 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -67,6 +67,7 @@ void MQTTCoverComponent::dump_config() { } } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson if (!this->cover_->get_device_class().empty()) root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); diff --git a/esphome/components/mqtt/mqtt_date.cpp b/esphome/components/mqtt/mqtt_date.cpp index 088a4788ed..0f0a334ae7 100644 --- a/esphome/components/mqtt/mqtt_date.cpp +++ b/esphome/components/mqtt/mqtt_date.cpp @@ -20,13 +20,13 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {} void MQTTDateComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { auto call = this->date_->make_call(); - if (root.containsKey("year")) { + if (root["year"].is()) { call.set_year(root["year"]); } - if (root.containsKey("month")) { + if (root["month"].is()) { call.set_month(root["month"]); } - if (root.containsKey("day")) { + if (root["day"].is()) { call.set_day(root["day"]); } call.perform(); @@ -55,6 +55,7 @@ bool MQTTDateComponent::send_initial_state() { } bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) { return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root["year"] = year; root["month"] = month; root["day"] = day; diff --git a/esphome/components/mqtt/mqtt_datetime.cpp b/esphome/components/mqtt/mqtt_datetime.cpp index 4ae6d0d416..5c56baabe0 100644 --- a/esphome/components/mqtt/mqtt_datetime.cpp +++ b/esphome/components/mqtt/mqtt_datetime.cpp @@ -20,22 +20,22 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim void MQTTDateTimeComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { auto call = this->datetime_->make_call(); - if (root.containsKey("year")) { + if (root["year"].is()) { call.set_year(root["year"]); } - if (root.containsKey("month")) { + if (root["month"].is()) { call.set_month(root["month"]); } - if (root.containsKey("day")) { + if (root["day"].is()) { call.set_day(root["day"]); } - if (root.containsKey("hour")) { + if (root["hour"].is()) { call.set_hour(root["hour"]); } - if (root.containsKey("minute")) { + if (root["minute"].is()) { call.set_minute(root["minute"]); } - if (root.containsKey("second")) { + if (root["second"].is()) { call.set_second(root["second"]); } call.perform(); @@ -68,6 +68,7 @@ bool MQTTDateTimeComponent::send_initial_state() { bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second) { return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root["year"] = year; root["month"] = month; root["day"] = day; diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index cf0b90e3d6..f972d545c6 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -16,7 +16,8 @@ using namespace esphome::event; MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {} void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - JsonArray event_types = root.createNestedArray(MQTT_EVENT_TYPES); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + JsonArray event_types = root[MQTT_EVENT_TYPES].to(); for (const auto &event_type : this->event_->get_event_types()) event_types.add(event_type); @@ -40,8 +41,10 @@ void MQTTEventComponent::dump_config() { } bool MQTTEventComponent::publish_event_(const std::string &event_type) { - return this->publish_json(this->get_state_topic_(), - [event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; }); + return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + root[MQTT_EVENT_TYPE] = event_type; + }); } std::string MQTTEventComponent::component_type() const { return "event"; } diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 35713bdab6..70e1ae3b4a 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -143,6 +143,7 @@ void MQTTFanComponent::dump_config() { bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson if (this->state_->get_traits().supports_direction()) { root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic(); root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic(); diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index f970da7d8c..4f5ff408a4 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -32,17 +32,21 @@ void MQTTJSONLightComponent::setup() { MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {} bool MQTTJSONLightComponent::publish_state_() { - return this->publish_json(this->get_state_topic_(), - [this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); }); + return this->publish_json(this->get_state_topic_(), [this](JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + LightJSONSchema::dump_json(*this->state_, root); + }); } LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root["schema"] = "json"; auto traits = this->state_->get_traits(); root[MQTT_COLOR_MODE] = true; - JsonArray color_modes = root.createNestedArray("supported_color_modes"); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + JsonArray color_modes = root["supported_color_modes"].to(); if (traits.supports_color_mode(ColorMode::ON_OFF)) color_modes.add("onoff"); if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) @@ -67,7 +71,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery if (this->state_->supports_effects()) { root["effect"] = true; - JsonArray effect_list = root.createNestedArray(MQTT_EFFECT_LIST); + JsonArray effect_list = root[MQTT_EFFECT_LIST].to(); for (auto *effect : this->state_->get_effects()) effect_list.add(effect->get_name()); effect_list.add("None"); diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index f4a5126d0c..0412624983 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -38,8 +38,10 @@ void MQTTLockComponent::dump_config() { std::string MQTTLockComponent::component_type() const { return "lock"; } const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - if (this->lock_->traits.get_assumed_state()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (this->lock_->traits.get_assumed_state()) { root[MQTT_OPTIMISTIC] = true; + } if (this->lock_->traits.get_supports_open()) root[MQTT_PAYLOAD_OPEN] = "OPEN"; } diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 3a6ea97967..a44632ff30 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -40,6 +40,7 @@ const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { const auto &traits = number_->traits; // https://www.home-assistant.io/integrations/number.mqtt/ + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root[MQTT_MIN] = traits.get_min_value(); root[MQTT_MAX] = traits.get_max_value(); root[MQTT_STEP] = traits.get_step(); diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index ea5130f823..b851348306 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -35,7 +35,8 @@ const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_ void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { const auto &traits = select_->traits; // https://www.home-assistant.io/integrations/select.mqtt/ - JsonArray options = root.createNestedArray(MQTT_OPTIONS); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + JsonArray options = root[MQTT_OPTIONS].to(); for (const auto &option : traits.get_options()) options.add(option); diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 2cbc291ccf..2e1db1908f 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -44,8 +44,10 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - if (!this->sensor_->get_device_class().empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (!this->sensor_->get_device_class().empty()) { root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + } if (!this->sensor_->get_unit_of_measurement().empty()) root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); @@ -74,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_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index 3fd578825a..8b1323bdb2 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -45,8 +45,10 @@ void MQTTSwitchComponent::dump_config() { std::string MQTTSwitchComponent::component_type() const { return "switch"; } const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - if (this->switch_->assumed_state()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (this->switch_->assumed_state()) { root[MQTT_OPTIMISTIC] = true; + } } bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } diff --git a/esphome/components/mqtt/mqtt_text.cpp b/esphome/components/mqtt/mqtt_text.cpp index cb852b64cd..5ab0ca9688 100644 --- a/esphome/components/mqtt/mqtt_text.cpp +++ b/esphome/components/mqtt/mqtt_text.cpp @@ -34,6 +34,7 @@ std::string MQTTTextComponent::component_type() const { return "text"; } const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson switch (this->text_->traits.get_mode()) { case TEXT_MODE_TEXT: root[MQTT_MODE] = "text"; diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index b0754bc8b3..42260ed2a8 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -15,8 +15,10 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - if (!this->sensor_->get_device_class().empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (!this->sensor_->get_device_class().empty()) { root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + } config.command_topic = false; } void MQTTTextSensor::setup() { @@ -38,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/mqtt/mqtt_time.cpp b/esphome/components/mqtt/mqtt_time.cpp index 332ef53cbc..0c95bd8147 100644 --- a/esphome/components/mqtt/mqtt_time.cpp +++ b/esphome/components/mqtt/mqtt_time.cpp @@ -20,13 +20,13 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {} void MQTTTimeComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { auto call = this->time_->make_call(); - if (root.containsKey("hour")) { + if (root["hour"].is()) { call.set_hour(root["hour"]); } - if (root.containsKey("minute")) { + if (root["minute"].is()) { call.set_minute(root["minute"]); } - if (root.containsKey("second")) { + if (root["second"].is()) { call.set_second(root["second"]); } call.perform(); @@ -55,6 +55,7 @@ bool MQTTTimeComponent::send_initial_state() { } bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) { return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root["hour"] = hour; root["minute"] = minute; root["second"] = second; diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp index 2ed8faf074..5d4807c7f3 100644 --- a/esphome/components/mqtt/mqtt_update.cpp +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -41,6 +41,7 @@ bool MQTTUpdateComponent::publish_state() { } void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson root["schema"] = "json"; root[MQTT_PAYLOAD_INSTALL] = "INSTALL"; } diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 85e06fe79c..551398cf42 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -49,8 +49,10 @@ void MQTTValveComponent::dump_config() { } } void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - if (!this->valve_->get_device_class().empty()) + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + if (!this->valve_->get_device_class().empty()) { root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); + } auto traits = this->valve_->get_traits(); if (traits.get_is_assumed_state()) { diff --git a/esphome/components/ms8607/ms8607.cpp b/esphome/components/ms8607/ms8607.cpp index b985623b24..f8ea26bfd9 100644 --- a/esphome/components/ms8607/ms8607.cpp +++ b/esphome/components/ms8607/ms8607.cpp @@ -356,7 +356,7 @@ void MS8607Component::read_humidity_(float temperature_float) { // map 16 bit humidity value into range [-6%, 118%] float const humidity_partial = double(humidity) / (1 << 16); - float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0); + float const humidity_percentage = std::lerp(-6.0, 118.0, humidity_partial); float const compensated_humidity_percentage = humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT; ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage); diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index cf5a7f5ef1..d3a2481693 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -1,5 +1,6 @@ #include "nfc.h" #include +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -7,29 +8,9 @@ namespace nfc { static const char *const TAG = "nfc"; -std::string format_uid(std::vector &uid) { - char buf[(uid.size() * 2) + uid.size() - 1]; - int offset = 0; - for (size_t i = 0; i < uid.size(); i++) { - const char *format = "%02X"; - if (i + 1 < uid.size()) - format = "%02X-"; - offset += sprintf(buf + offset, format, uid[i]); - } - return std::string(buf); -} +std::string format_uid(const std::vector &uid) { return format_hex_pretty(uid, '-', false); } -std::string format_bytes(std::vector &bytes) { - char buf[(bytes.size() * 2) + bytes.size() - 1]; - int offset = 0; - for (size_t i = 0; i < bytes.size(); i++) { - const char *format = "%02X"; - if (i + 1 < bytes.size()) - format = "%02X "; - offset += sprintf(buf + offset, format, bytes[i]); - } - return std::string(buf); -} +std::string format_bytes(const std::vector &bytes) { return format_hex_pretty(bytes, ' ', false); } uint8_t guess_tag_type(uint8_t uid_length) { if (uid_length == 4) { diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index 2e5c5cd9c5..9879cfdb03 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -2,8 +2,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "ndef_record.h" #include "ndef_message.h" +#include "ndef_record.h" #include "nfc_tag.h" #include @@ -53,8 +53,8 @@ static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; -std::string format_uid(std::vector &uid); -std::string format_bytes(std::vector &bytes); +std::string format_uid(const std::vector &uid); +std::string format_bytes(const std::vector &bytes); uint8_t guess_tag_type(uint8_t uid_length); uint8_t get_mifare_classic_ndef_start_index(std::vector &data); 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/online_image/__init__.py b/esphome/components/online_image/__init__.py index 3f15db6e50..7a6d25bc7d 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -2,7 +2,7 @@ import logging from esphome import automation import esphome.codegen as cg -from esphome.components.const import CONF_REQUEST_HEADERS +from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.image import ( CONF_INVERT_ALPHA, @@ -11,6 +11,7 @@ from esphome.components.image import ( Image_, get_image_type_enum, get_transparency_enum, + validate_settings, ) import esphome.config_validation as cv from esphome.const import ( @@ -161,6 +162,7 @@ CONFIG_SCHEMA = cv.Schema( rp2040_arduino=cv.Version(0, 0, 0), host=cv.Version(0, 0, 0), ), + validate_settings, ) ) @@ -213,6 +215,7 @@ async def to_code(config): get_image_type_enum(config[CONF_TYPE]), transparent, config[CONF_BUFFER_SIZE], + config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN", ) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index d0c743ef93..4e2ecc2c77 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -35,14 +35,15 @@ inline bool is_color_on(const Color &color) { } OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, - image::Transparency transparency, uint32_t download_buffer_size) + image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian) : Image(nullptr, 0, 0, type, transparency), buffer_(nullptr), download_buffer_(download_buffer_size), download_buffer_initial_size_(download_buffer_size), format_(format), fixed_width_(width), - fixed_height_(height) { + fixed_height_(height), + is_big_endian_(is_big_endian) { this->set_url(url); } @@ -296,7 +297,7 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { break; } case ImageType::IMAGE_TYPE_GRAYSCALE: { - uint8_t gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); + auto gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { if (gray == 1) { gray = 0; @@ -314,8 +315,13 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { case ImageType::IMAGE_TYPE_RGB565: { this->map_chroma_key(color); uint16_t col565 = display::ColorUtil::color_to_565(color); - this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); - this->buffer_[pos + 1] = static_cast(col565 & 0xFF); + if (this->is_big_endian_) { + this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); + this->buffer_[pos + 1] = static_cast(col565 & 0xFF); + } else { + this->buffer_[pos + 0] = static_cast(col565 & 0xFF); + this->buffer_[pos + 1] = static_cast((col565 >> 8) & 0xFF); + } if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { this->buffer_[pos + 2] = color.w; } diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 6a2144538f..3326cbe8d6 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -50,7 +50,7 @@ class OnlineImage : public PollingComponent, * @param buffer_size Size of the buffer used to download the image. */ OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, - image::Transparency transparency, uint32_t buffer_size); + image::Transparency transparency, uint32_t buffer_size, bool is_big_endian); void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; @@ -164,6 +164,11 @@ class OnlineImage : public PollingComponent, const int fixed_width_; /** height requested on configuration, or 0 if non specified. */ const int fixed_height_; + /** + * Whether the image is stored in big-endian format. + * This is used to determine how to store 16 bit colors in the buffer. + */ + bool is_big_endian_; /** * Actual width of the current image. If fixed_width_ is specified, * this will be equal to it; otherwise it will be set once the decoding diff --git a/esphome/components/opentherm/output/output.cpp b/esphome/components/opentherm/output/output.cpp index f820dc76f1..486aa0d4e7 100644 --- a/esphome/components/opentherm/output/output.cpp +++ b/esphome/components/opentherm/output/output.cpp @@ -10,7 +10,7 @@ void opentherm::OpenthermOutput::write_state(float state) { ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_); this->state = state < 0.003 && this->zero_means_zero_ ? 0.0 - : clamp(lerp(state, min_value_, max_value_), min_value_, max_value_); + : clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_); this->has_state_ = true; ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); } diff --git a/esphome/components/opt3001/sensor.py b/esphome/components/opt3001/sensor.py index a5bbf0e8dd..8490b0bd49 100644 --- a/esphome/components/opt3001/sensor.py +++ b/esphome/components/opt3001/sensor.py @@ -1,11 +1,7 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import ( - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, - UNIT_LUX, -) +import esphome.config_validation as cv +from esphome.const import DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT, UNIT_LUX DEPENDENCIES = ["i2c"] CODEOWNERS = ["@ccutrer"] diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 6684d43ff7..b6ce24bc1b 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -314,6 +314,9 @@ void PacketTransport::send_data_(bool all) { } void PacketTransport::update() { + if (!this->ping_pong_enable_) { + return; + } auto now = millis() / 1000; if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) { this->resend_ping_key_ = this->ping_pong_enable_; diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index ecbeb83bb4..28c3bbd70c 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_RP2040, + CoreModel, ) from esphome.core import CORE, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, mkdir_p, read_file, write_file @@ -165,11 +166,13 @@ async def to_code(config): # Allow LDF to properly discover dependency including those in preprocessor # conditionals cg.add_platformio_option("lib_ldf_mode", "chain+") + cg.add_platformio_option("lib_compat_mode", "strict") cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_build_flag("-DUSE_RP2040") cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "RP2040") + cg.add_define(CoreModel.SINGLE) cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) @@ -203,7 +206,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/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index e55bff58fe..5ad18f6311 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -48,6 +48,9 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t * } void Sdl::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y)) + return; + SDL_Rect rect{x, y, 1, 1}; auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB)); SDL_UpdateTexture(this->texture_, &rect, &data, 2); diff --git a/esphome/components/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/servo/servo.cpp b/esphome/components/servo/servo.cpp index b8546d345c..b4511de2d0 100644 --- a/esphome/components/servo/servo.cpp +++ b/esphome/components/servo/servo.cpp @@ -88,9 +88,9 @@ void Servo::internal_write(float value) { value = clamp(value, -1.0f, 1.0f); float level; if (value < 0.0) { - level = lerp(-value, this->idle_level_, this->min_level_); + level = std::lerp(this->idle_level_, this->min_level_, -value); } else { - level = lerp(value, this->idle_level_, this->max_level_); + level = std::lerp(this->idle_level_, this->max_level_, value); } this->output_->set_level(level); this->current_value_ = value; 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/__init__.py b/esphome/components/ssd1306_base/__init__.py index ab2c7a5496..6633b24607 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -6,6 +6,8 @@ from esphome.const import ( CONF_BRIGHTNESS, CONF_CONTRAST, CONF_EXTERNAL_VCC, + CONF_FLIP_X, + CONF_FLIP_Y, CONF_INVERT, CONF_LAMBDA, CONF_MODEL, @@ -18,9 +20,6 @@ ssd1306_base_ns = cg.esphome_ns.namespace("ssd1306_base") SSD1306 = ssd1306_base_ns.class_("SSD1306", cg.PollingComponent, display.DisplayBuffer) SSD1306Model = ssd1306_base_ns.enum("SSD1306Model") -CONF_FLIP_X = "flip_x" -CONF_FLIP_Y = "flip_y" - MODELS = { "SSD1306_128X32": SSD1306Model.SSD1306_MODEL_128_32, "SSD1306_128X64": SSD1306Model.SSD1306_MODEL_128_64, 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/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 5878af43b2..e494529b9e 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -5,13 +5,8 @@ from esphome.config_helpers import Extend, Remove, merge_config import esphome.config_validation as cv from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS from esphome.yaml_util import ESPHomeDataBase, make_data_base -from .jinja import ( - Jinja, - JinjaStr, - has_jinja, - TemplateError, - TemplateRuntimeError, -) + +from .jinja import Jinja, JinjaStr, TemplateError, TemplateRuntimeError, has_jinja CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) @@ -151,8 +146,11 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing): if sub is not None: item[k] = sub for old, new in replace_keys: - item[new] = merge_config(item.get(old), item.get(new)) - del item[old] + if str(new) == str(old): + item[new] = item[old] + else: + item[new] = merge_config(item.get(old), item.get(new)) + del item[old] elif isinstance(item, str): sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing) if isinstance(sub, JinjaStr) or sub != item: diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py index 9ecdbab844..cf393d2a5d 100644 --- a/esphome/components/substitutions/jinja.py +++ b/esphome/components/substitutions/jinja.py @@ -1,6 +1,7 @@ import logging import math import re + import jinja2 as jinja from jinja2.nativetypes import NativeEnvironment diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 492febe283..b6aeaf072c 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -167,8 +167,8 @@ def validate_config(config): if config[CONF_MODULATION] == "LORA": if config[CONF_BANDWIDTH] not in lora_bws: raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA") - if config[CONF_PREAMBLE_SIZE] > 0 and config[CONF_PREAMBLE_SIZE] < 6: - raise cv.Invalid("Minimum preamble size is 6 with LORA") + if config[CONF_PREAMBLE_SIZE] < 6: + raise cv.Invalid("Minimum 'preamble_size' is 6 with LORA") if config[CONF_SPREADING_FACTOR] == 6 and config[CONF_PAYLOAD_LENGTH] == 0: raise cv.Invalid("Payload length must be set when spreading factor is 6") else: @@ -200,7 +200,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP), cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256), cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4), - cv.Required(CONF_PREAMBLE_SIZE): cv.int_range(min=1, max=65535), + cv.Optional(CONF_PREAMBLE_SIZE, default=8): cv.int_range(min=1, max=65535), cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema, cv.Optional(CONF_RX_START, default=True): cv.boolean, cv.Required(CONF_RF_SWITCH): cv.boolean, diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index 4d034801cc..33b556db07 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -164,8 +164,8 @@ def validate_config(config): raise cv.Invalid(f"{config[CONF_BANDWIDTH]} is not available with LORA") if CONF_DIO0_PIN not in config: raise cv.Invalid("Cannot use LoRa without dio0_pin") - if 0 < config[CONF_PREAMBLE_SIZE] < 6: - raise cv.Invalid("Minimum preamble size is 6 with LORA") + if config[CONF_PREAMBLE_SIZE] < 6: + raise cv.Invalid("Minimum 'preamble_size' is 6 with LORA") if config[CONF_SPREADING_FACTOR] == 6 and config[CONF_PAYLOAD_LENGTH] == 0: raise cv.Invalid("Payload length must be set when spreading factor is 6") else: 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/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index fc5f772f41..edf6c94b07 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -70,7 +70,7 @@ static void usbh_print_cfg_desc(const usb_config_desc_t *cfg_desc) { ESP_LOGV(TAG, "bMaxPower %dmA", cfg_desc->bMaxPower * 2); } -void usb_client_print_device_descriptor(const usb_device_desc_t *devc_desc) { +static void usb_client_print_device_descriptor(const usb_device_desc_t *devc_desc) { if (devc_desc == NULL) { return; } @@ -92,8 +92,8 @@ void usb_client_print_device_descriptor(const usb_device_desc_t *devc_desc) { ESP_LOGV(TAG, "bNumConfigurations %d", devc_desc->bNumConfigurations); } -void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc, - print_class_descriptor_cb class_specific_cb) { +static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc, + print_class_descriptor_cb class_specific_cb) { if (cfg_desc == nullptr) { return; } @@ -128,9 +128,9 @@ void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc, static std::string get_descriptor_string(const usb_str_desc_t *desc) { char buffer[256]; if (desc == nullptr) - return "(unknown)"; + return "(unspecified)"; char *p = buffer; - for (size_t i = 0; i != desc->bLength / 2; i++) { + for (int i = 0; i != desc->bLength / 2; i++) { auto c = desc->wData[i]; if (c < 0x100) *p++ = static_cast(c); @@ -169,7 +169,7 @@ void USBClient::setup() { this->mark_failed(); return; } - for (auto trq : this->trq_pool_) { + for (auto *trq : this->trq_pool_) { usb_host_transfer_alloc(64, 0, &trq->transfer); trq->client = this; } @@ -197,7 +197,8 @@ void USBClient::loop() { ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct); if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) { usb_device_info_t dev_info; - if ((err = usb_host_device_info(this->device_handle_, &dev_info)) != ESP_OK) { + err = usb_host_device_info(this->device_handle_, &dev_info); + if (err != ESP_OK) { ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err)); this->disconnect(); break; @@ -336,7 +337,7 @@ static void transfer_callback(usb_transfer_t *xfer) { * @throws None. */ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) { - auto trq = this->get_trq_(); + auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); return; @@ -349,7 +350,6 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err); this->release_trq(trq); - this->disconnect(); } } @@ -364,7 +364,7 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u * @throws None. */ void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) { - auto trq = this->get_trq_(); + auto *trq = this->get_trq_(); if (trq == nullptr) { ESP_LOGE(TAG, "Too many requests queued"); return; diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index 267385d1bd..f7d60c307a 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -43,7 +43,7 @@ static constexpr uint8_t SET_BAUDRATE = 0x1E; // Set the baud rate. static constexpr uint8_t SET_CHARS = 0x19; // Set special characters. static constexpr uint8_t VENDOR_SPECIFIC = 0xFF; // Vendor specific command. -std::vector USBUartTypeCP210X::parse_descriptors_(usb_device_handle_t dev_hdl) { +std::vector USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev_hdl) { const usb_config_desc_t *config_desc; const usb_device_desc_t *device_desc; int conf_offset = 0, ep_offset; diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index e599409f0c..934306f480 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -18,52 +18,48 @@ namespace usb_uart { */ static optional get_cdc(const usb_config_desc_t *config_desc, uint8_t intf_idx) { int conf_offset, ep_offset; - const usb_ep_desc_t *notify_ep{}, *in_ep{}, *out_ep{}; - uint8_t interface_number = 0; - // look for an interface with one interrupt endpoint (notify), and an interface with two bulk endpoints (data in/out) + // look for an interface with an interrupt endpoint (notify), and one with two bulk endpoints (data in/out) + CdcEps eps{}; + eps.bulk_interface_number = 0xFF; for (;;) { - auto intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx++, 0, &conf_offset); + const auto *intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx++, 0, &conf_offset); if (!intf_desc) { ESP_LOGE(TAG, "usb_parse_interface_descriptor failed"); return nullopt; } - if (intf_desc->bNumEndpoints == 1) { + ESP_LOGD(TAG, "intf_desc: bInterfaceClass=%02X, bInterfaceSubClass=%02X, bInterfaceProtocol=%02X, bNumEndpoints=%d", + intf_desc->bInterfaceClass, intf_desc->bInterfaceSubClass, intf_desc->bInterfaceProtocol, + intf_desc->bNumEndpoints); + for (uint8_t i = 0; i != intf_desc->bNumEndpoints; i++) { ep_offset = conf_offset; - notify_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 0, config_desc->wTotalLength, &ep_offset); - if (!notify_ep) { - ESP_LOGE(TAG, "notify_ep: usb_parse_endpoint_descriptor_by_index failed"); + const auto *ep = usb_parse_endpoint_descriptor_by_index(intf_desc, i, config_desc->wTotalLength, &ep_offset); + if (!ep) { + ESP_LOGE(TAG, "Ran out of interfaces at %d before finding all endpoints", i); return nullopt; } - if (notify_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_INT) - notify_ep = nullptr; - } else if (USB_CLASS_CDC_DATA && intf_desc->bNumEndpoints == 2) { - interface_number = intf_desc->bInterfaceNumber; - ep_offset = conf_offset; - out_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 0, config_desc->wTotalLength, &ep_offset); - if (!out_ep) { - ESP_LOGE(TAG, "out_ep: usb_parse_endpoint_descriptor_by_index failed"); - return nullopt; + ESP_LOGD(TAG, "ep: bEndpointAddress=%02X, bmAttributes=%02X", ep->bEndpointAddress, ep->bmAttributes); + if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_INT) { + eps.notify_ep = ep; + eps.interrupt_interface_number = intf_desc->bInterfaceNumber; + } else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && ep->bEndpointAddress & usb_host::USB_DIR_IN && + (eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) { + eps.in_ep = ep; + eps.bulk_interface_number = intf_desc->bInterfaceNumber; + } else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && !(ep->bEndpointAddress & usb_host::USB_DIR_IN) && + (eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) { + eps.out_ep = ep; + eps.bulk_interface_number = intf_desc->bInterfaceNumber; + } else { + ESP_LOGE(TAG, "Unexpected endpoint attributes: %02X", ep->bmAttributes); + continue; } - if (out_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_BULK) - out_ep = nullptr; - ep_offset = conf_offset; - in_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 1, config_desc->wTotalLength, &ep_offset); - if (!in_ep) { - ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed"); - return nullopt; - } - if (in_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_BULK) - in_ep = nullptr; } - if (in_ep != nullptr && out_ep != nullptr && notify_ep != nullptr) - break; + if (eps.in_ep != nullptr && eps.out_ep != nullptr && eps.notify_ep != nullptr) + return eps; } - if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN) - return CdcEps{notify_ep, in_ep, out_ep, interface_number}; - return CdcEps{notify_ep, out_ep, in_ep, interface_number}; } -std::vector USBUartTypeCdcAcm::parse_descriptors_(usb_device_handle_t dev_hdl) { +std::vector USBUartTypeCdcAcm::parse_descriptors(usb_device_handle_t dev_hdl) { const usb_config_desc_t *config_desc; const usb_device_desc_t *device_desc; int desc_offset = 0; @@ -78,7 +74,7 @@ std::vector USBUartTypeCdcAcm::parse_descriptors_(usb_device_handle_t de ESP_LOGE(TAG, "get_active_config_descriptor failed"); return {}; } - if (device_desc->bDeviceClass == USB_CLASS_COMM) { + if (device_desc->bDeviceClass == USB_CLASS_COMM || device_desc->bDeviceClass == USB_CLASS_VENDOR_SPEC) { // single CDC-ACM device if (auto eps = get_cdc(config_desc, 0)) { ESP_LOGV(TAG, "Found CDC-ACM device"); @@ -194,7 +190,7 @@ void USBUartComponent::start_input(USBUartChannel *channel) { if (!channel->initialised_ || channel->input_started_ || channel->input_buffer_.get_free_space() < channel->cdc_dev_.in_ep->wMaxPacketSize) return; - auto ep = channel->cdc_dev_.in_ep; + const auto *ep = channel->cdc_dev_.in_ep; auto callback = [this, channel](const usb_host::TransferStatus &status) { ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code); if (!status.success) { @@ -227,7 +223,7 @@ void USBUartComponent::start_output(USBUartChannel *channel) { if (channel->output_buffer_.is_empty()) { return; } - auto ep = channel->cdc_dev_.out_ep; + const auto *ep = channel->cdc_dev_.out_ep; auto callback = [this, channel](const usb_host::TransferStatus &status) { ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code); channel->output_started_ = false; @@ -259,15 +255,15 @@ static void fix_mps(const usb_ep_desc_t *ep) { } } void USBUartTypeCdcAcm::on_connected() { - auto cdc_devs = this->parse_descriptors_(this->device_handle_); + auto cdc_devs = this->parse_descriptors(this->device_handle_); if (cdc_devs.empty()) { this->status_set_error("No CDC-ACM device found"); this->disconnect(); return; } ESP_LOGD(TAG, "Found %zu CDC-ACM devices", cdc_devs.size()); - auto i = 0; - for (auto channel : this->channels_) { + size_t i = 0; + for (auto *channel : this->channels_) { if (i == cdc_devs.size()) { ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_); this->status_set_warning("No configuration found for channel"); @@ -277,10 +273,11 @@ void USBUartTypeCdcAcm::on_connected() { fix_mps(channel->cdc_dev_.in_ep); fix_mps(channel->cdc_dev_.out_ep); channel->initialised_ = true; - auto err = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.interface_number, 0); + auto err = + usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number, 0); if (err != ESP_OK) { ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_, - channel->cdc_dev_.interface_number); + channel->cdc_dev_.bulk_interface_number); this->status_set_error("usb_host_interface_claim failed"); this->disconnect(); return; @@ -290,7 +287,7 @@ void USBUartTypeCdcAcm::on_connected() { } void USBUartTypeCdcAcm::on_disconnected() { - for (auto channel : this->channels_) { + for (auto *channel : this->channels_) { if (channel->cdc_dev_.in_ep != nullptr) { usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress); usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress); @@ -303,7 +300,7 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress); } - usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interface_number); + usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number); channel->initialised_ = false; channel->input_started_ = false; channel->output_started_ = false; @@ -314,7 +311,7 @@ void USBUartTypeCdcAcm::on_disconnected() { } void USBUartTypeCdcAcm::enable_channels() { - for (auto channel : this->channels_) { + for (auto *channel : this->channels_) { if (!channel->initialised_) continue; channel->input_started_ = false; diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index fd0fb2c59a..a103c51add 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -25,7 +25,8 @@ struct CdcEps { const usb_ep_desc_t *notify_ep; const usb_ep_desc_t *in_ep; const usb_ep_desc_t *out_ep; - uint8_t interface_number; + uint8_t bulk_interface_number; + uint8_t interrupt_interface_number; }; enum UARTParityOptions { @@ -123,7 +124,7 @@ class USBUartTypeCdcAcm : public USBUartComponent { USBUartTypeCdcAcm(uint16_t vid, uint16_t pid) : USBUartComponent(vid, pid) {} protected: - virtual std::vector parse_descriptors_(usb_device_handle_t dev_hdl); + virtual std::vector parse_descriptors(usb_device_handle_t dev_hdl); void on_connected() override; virtual void enable_channels(); void on_disconnected() override; @@ -134,7 +135,7 @@ class USBUartTypeCP210X : public USBUartTypeCdcAcm { USBUartTypeCP210X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {} protected: - std::vector parse_descriptors_(usb_device_handle_t dev_hdl) override; + std::vector parse_descriptors(usb_device_handle_t dev_hdl) override; void enable_channels() override; }; class USBUartTypeCH34X : public USBUartTypeCdcAcm { 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..7211f707e9 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) @@ -57,7 +76,7 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) { percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); } else { - ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); + ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_); } #ifdef USE_OTA_STATE_CALLBACK // Report progress - use call_deferred since we're in web server task @@ -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=%" PRIu32 ", 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 8ced5b7e18..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"; @@ -792,7 +792,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi light::LightJSONSchema::dump_json(*obj, root); if (start_config == DETAIL_ALL) { - JsonArray opt = root.createNestedArray("effects"); + JsonArray opt = root["effects"].to(); opt.add("None"); for (auto const &option : obj->get_effects()) { opt.add(option->get_name()); @@ -1238,7 +1238,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value return json::build_json([this, obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); if (start_config == DETAIL_ALL) { - JsonArray opt = root.createNestedArray("option"); + JsonArray opt = root["option"].to(); for (auto &option : obj->traits.get_options()) { opt.add(option); } @@ -1322,6 +1322,7 @@ std::string WebServer::climate_all_json_generator(WebServer *web_server, void *s return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL); } std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); const auto traits = obj->get_traits(); @@ -1330,32 +1331,32 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf char buf[16]; if (start_config == DETAIL_ALL) { - JsonArray opt = root.createNestedArray("modes"); + JsonArray opt = root["modes"].to(); for (climate::ClimateMode m : traits.get_supported_modes()) opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); if (!traits.get_supported_custom_fan_modes().empty()) { - JsonArray opt = root.createNestedArray("fan_modes"); + JsonArray opt = root["fan_modes"].to(); for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); } if (!traits.get_supported_custom_fan_modes().empty()) { - JsonArray opt = root.createNestedArray("custom_fan_modes"); + JsonArray opt = root["custom_fan_modes"].to(); for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) opt.add(custom_fan_mode); } if (traits.get_supports_swing_modes()) { - JsonArray opt = root.createNestedArray("swing_modes"); + JsonArray opt = root["swing_modes"].to(); for (auto swing_mode : traits.get_supported_swing_modes()) opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); } if (traits.get_supports_presets() && obj->preset.has_value()) { - JsonArray opt = root.createNestedArray("presets"); + JsonArray opt = root["presets"].to(); for (climate::ClimatePreset m : traits.get_supported_presets()) opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); } if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { - JsonArray opt = root.createNestedArray("custom_presets"); + JsonArray opt = root["custom_presets"].to(); for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); } @@ -1407,6 +1408,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf root["state"] = root["target_temperature"]; } }); + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } #endif @@ -1618,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); @@ -1635,7 +1639,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty root["event_type"] = event_type; } if (start_config == DETAIL_ALL) { - JsonArray event_types = root.createNestedArray("event_types"); + JsonArray event_types = root["event_types"].to(); for (auto const &event_type : obj->get_event_types()) { event_types.add(event_type); } @@ -1682,6 +1686,7 @@ std::string WebServer::update_all_json_generator(WebServer *web_server, void *so return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); } std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); root["value"] = obj->update_info.latest_version; @@ -1707,166 +1712,166 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c this->add_sorting_info_(root, obj); } }); + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } #endif bool WebServer::canHandle(AsyncWebServerRequest *request) const { - if (request->url() == "/") + const auto &url = request->url(); + const auto method = request->method(); + + // Simple URL checks + if (url == "/") return true; #ifdef USE_ARDUINO - if (request->url() == "/events") { + if (url == "/events") return true; - } #endif #ifdef USE_WEBSERVER_CSS_INCLUDE - if (request->url() == "/0.css") + if (url == "/0.css") return true; #endif #ifdef USE_WEBSERVER_JS_INCLUDE - if (request->url() == "/0.js") + if (url == "/0.js") return true; #endif #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS - if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) { + if (method == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) return true; - } #endif - // Store the URL to prevent temporary string destruction - // request->url() returns a reference to a String (on Arduino) or std::string (on ESP-IDF) - // UrlMatch stores pointers to the string's data, so we must ensure the string outlives match_url() - const auto &url = request->url(); + // Parse URL for component checks UrlMatch match = match_url(url.c_str(), url.length(), true); if (!match.valid) return false; + + // Common pattern check + bool is_get = method == HTTP_GET; + bool is_post = method == HTTP_POST; + bool is_get_or_post = is_get || is_post; + + if (!is_get_or_post) + return false; + + // GET-only components + if (is_get) { #ifdef USE_SENSOR - if (request->method() == HTTP_GET && match.domain_equals("sensor")) - return true; + if (match.domain_equals("sensor")) + return true; #endif - -#ifdef USE_SWITCH - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("switch")) - return true; -#endif - -#ifdef USE_BUTTON - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("button")) - return true; -#endif - #ifdef USE_BINARY_SENSOR - if (request->method() == HTTP_GET && match.domain_equals("binary_sensor")) - return true; + if (match.domain_equals("binary_sensor")) + return true; #endif - -#ifdef USE_FAN - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("fan")) - return true; -#endif - -#ifdef USE_LIGHT - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("light")) - return true; -#endif - #ifdef USE_TEXT_SENSOR - if (request->method() == HTTP_GET && match.domain_equals("text_sensor")) - return true; + if (match.domain_equals("text_sensor")) + return true; #endif - -#ifdef USE_COVER - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("cover")) - return true; -#endif - -#ifdef USE_NUMBER - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("number")) - return true; -#endif - -#ifdef USE_DATETIME_DATE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("date")) - return true; -#endif - -#ifdef USE_DATETIME_TIME - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("time")) - return true; -#endif - -#ifdef USE_DATETIME_DATETIME - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("datetime")) - return true; -#endif - -#ifdef USE_TEXT - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("text")) - return true; -#endif - -#ifdef USE_SELECT - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("select")) - return true; -#endif - -#ifdef USE_CLIMATE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("climate")) - return true; -#endif - -#ifdef USE_LOCK - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("lock")) - return true; -#endif - -#ifdef USE_VALVE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("valve")) - return true; -#endif - -#ifdef USE_ALARM_CONTROL_PANEL - if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain_equals("alarm_control_panel")) - return true; -#endif - #ifdef USE_EVENT - if (request->method() == HTTP_GET && match.domain_equals("event")) - return true; + if (match.domain_equals("event")) + return true; #endif + } -#ifdef USE_UPDATE - if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("update")) - return true; + // GET+POST components + if (is_get_or_post) { +#ifdef USE_SWITCH + if (match.domain_equals("switch")) + return true; #endif +#ifdef USE_BUTTON + if (match.domain_equals("button")) + return true; +#endif +#ifdef USE_FAN + if (match.domain_equals("fan")) + return true; +#endif +#ifdef USE_LIGHT + if (match.domain_equals("light")) + return true; +#endif +#ifdef USE_COVER + if (match.domain_equals("cover")) + return true; +#endif +#ifdef USE_NUMBER + if (match.domain_equals("number")) + return true; +#endif +#ifdef USE_DATETIME_DATE + if (match.domain_equals("date")) + return true; +#endif +#ifdef USE_DATETIME_TIME + if (match.domain_equals("time")) + return true; +#endif +#ifdef USE_DATETIME_DATETIME + if (match.domain_equals("datetime")) + return true; +#endif +#ifdef USE_TEXT + if (match.domain_equals("text")) + return true; +#endif +#ifdef USE_SELECT + if (match.domain_equals("select")) + return true; +#endif +#ifdef USE_CLIMATE + if (match.domain_equals("climate")) + return true; +#endif +#ifdef USE_LOCK + if (match.domain_equals("lock")) + return true; +#endif +#ifdef USE_VALVE + if (match.domain_equals("valve")) + return true; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + if (match.domain_equals("alarm_control_panel")) + return true; +#endif +#ifdef USE_UPDATE + if (match.domain_equals("update")) + return true; +#endif + } return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { - if (request->url() == "/") { + const auto &url = request->url(); + + // Handle static routes first + if (url == "/") { this->handle_index_request(request); return; } #ifdef USE_ARDUINO - if (request->url() == "/events") { + if (url == "/events") { this->events_.add_new_client(this, request); return; } #endif #ifdef USE_WEBSERVER_CSS_INCLUDE - if (request->url() == "/0.css") { + if (url == "/0.css") { this->handle_css_request(request); return; } #endif #ifdef USE_WEBSERVER_JS_INCLUDE - if (request->url() == "/0.js") { + if (url == "/0.js") { this->handle_js_request(request); return; } @@ -1879,147 +1884,85 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { } #endif - // See comment in canHandle() for why we store the URL reference - const auto &url = request->url(); + // Parse URL for component routing UrlMatch match = match_url(url.c_str(), url.length(), false); + // Component routing using minimal code repetition + struct ComponentRoute { + const char *domain; + void (WebServer::*handler)(AsyncWebServerRequest *, const UrlMatch &); + }; + + static const ComponentRoute ROUTES[] = { #ifdef USE_SENSOR - if (match.domain_equals("sensor")) { - this->handle_sensor_request(request, match); - return; - } + {"sensor", &WebServer::handle_sensor_request}, #endif - #ifdef USE_SWITCH - if (match.domain_equals("switch")) { - this->handle_switch_request(request, match); - return; - } + {"switch", &WebServer::handle_switch_request}, #endif - #ifdef USE_BUTTON - if (match.domain_equals("button")) { - this->handle_button_request(request, match); - return; - } + {"button", &WebServer::handle_button_request}, #endif - #ifdef USE_BINARY_SENSOR - if (match.domain_equals("binary_sensor")) { - this->handle_binary_sensor_request(request, match); - return; - } + {"binary_sensor", &WebServer::handle_binary_sensor_request}, #endif - #ifdef USE_FAN - if (match.domain_equals("fan")) { - this->handle_fan_request(request, match); - return; - } + {"fan", &WebServer::handle_fan_request}, #endif - #ifdef USE_LIGHT - if (match.domain_equals("light")) { - this->handle_light_request(request, match); - return; - } + {"light", &WebServer::handle_light_request}, #endif - #ifdef USE_TEXT_SENSOR - if (match.domain_equals("text_sensor")) { - this->handle_text_sensor_request(request, match); - return; - } + {"text_sensor", &WebServer::handle_text_sensor_request}, #endif - #ifdef USE_COVER - if (match.domain_equals("cover")) { - this->handle_cover_request(request, match); - return; - } + {"cover", &WebServer::handle_cover_request}, #endif - #ifdef USE_NUMBER - if (match.domain_equals("number")) { - this->handle_number_request(request, match); - return; - } + {"number", &WebServer::handle_number_request}, #endif - #ifdef USE_DATETIME_DATE - if (match.domain_equals("date")) { - this->handle_date_request(request, match); - return; - } + {"date", &WebServer::handle_date_request}, #endif - #ifdef USE_DATETIME_TIME - if (match.domain_equals("time")) { - this->handle_time_request(request, match); - return; - } + {"time", &WebServer::handle_time_request}, #endif - #ifdef USE_DATETIME_DATETIME - if (match.domain_equals("datetime")) { - this->handle_datetime_request(request, match); - return; - } + {"datetime", &WebServer::handle_datetime_request}, #endif - #ifdef USE_TEXT - if (match.domain_equals("text")) { - this->handle_text_request(request, match); - return; - } + {"text", &WebServer::handle_text_request}, #endif - #ifdef USE_SELECT - if (match.domain_equals("select")) { - this->handle_select_request(request, match); - return; - } + {"select", &WebServer::handle_select_request}, #endif - #ifdef USE_CLIMATE - if (match.domain_equals("climate")) { - this->handle_climate_request(request, match); - return; - } + {"climate", &WebServer::handle_climate_request}, #endif - #ifdef USE_LOCK - if (match.domain_equals("lock")) { - this->handle_lock_request(request, match); - - return; - } + {"lock", &WebServer::handle_lock_request}, #endif - #ifdef USE_VALVE - if (match.domain_equals("valve")) { - this->handle_valve_request(request, match); - return; - } + {"valve", &WebServer::handle_valve_request}, #endif - #ifdef USE_ALARM_CONTROL_PANEL - if (match.domain_equals("alarm_control_panel")) { - this->handle_alarm_control_panel_request(request, match); - - return; - } + {"alarm_control_panel", &WebServer::handle_alarm_control_panel_request}, #endif - #ifdef USE_UPDATE - if (match.domain_equals("update")) { - this->handle_update_request(request, match); - return; - } + {"update", &WebServer::handle_update_request}, #endif + }; + + // Check each route + for (const auto &route : ROUTES) { + if (match.domain_equals(route.domain)) { + (this->*route.handler)(request, match); + return; + } + } // No matching handler found - send 404 - ESP_LOGV(TAG, "Request for unknown URL: %s", request->url().c_str()); + ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str()); request->send(404, "text/plain", "Not Found"); } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 0c15881d1e..ef1b03a73b 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -78,7 +78,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; This is because only minimal changes were made to the ESPAsyncWebServer lib_dep, it was undesirable to put deferred update logic into that library. We need one deferred queue per connection so instead of one AsyncEventSource with multiple clients, we have multiple event sources with one client each. This is slightly awkward which is why it's - implemented in a more straightforward way for ESP-IDF. Arudino platform will eventually go away and this workaround + implemented in a more straightforward way for ESP-IDF. Arduino platform will eventually go away and this workaround can be forgotten. */ #ifdef USE_ARDUINO 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/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 754bf7d433..9f3371c233 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -40,4 +40,4 @@ async def to_code(config): if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json - cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8") + cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10") diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index d2447681f5..734259093e 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -389,10 +389,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * #ifdef USE_WEBSERVER_SORTING for (auto &group : ws->sorting_groups_) { + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson message = json::build_json([group](JsonObject root) { root["name"] = group.second.name; root["sorting_weight"] = group.second.weight; }); + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) // a (very) large number of these should be able to be queued initially without defer // since the only thing in the send buffer at this point is the initial ping/config 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/config_validation.py b/esphome/config_validation.py index 09b132a458..b1691fa43e 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1055,6 +1055,7 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False): return validator +bps = float_with_unit("bits per second", "(bps|bits/s|bit/s)?") frequency = float_with_unit("frequency", "(Hz|HZ|hz)?") resistance = float_with_unit("resistance", "(Ω|Ω|ohm|Ohm|OHM)?") current = float_with_unit("current", "(a|A|amp|Amp|amps|Amps|ampere|Ampere)?") diff --git a/esphome/const.py b/esphome/const.py index 085b9b39b8..627b6bac18 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.7.0-dev" +__version__ = "2025.8.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( @@ -21,6 +21,7 @@ class Platform(StrEnum): HOST = "host" LIBRETINY_OLDSTYLE = "libretiny" LN882X = "ln882x" + NRF52 = "nrf52" RP2040 = "rp2040" RTL87XX = "rtl87xx" @@ -31,6 +32,15 @@ class Framework(StrEnum): ARDUINO = "arduino" ESP_IDF = "esp-idf" NATIVE = "host" + ZEPHYR = "zephyr" + + +class CoreModel(StrEnum): + """Core model identifiers for ESPHome scheduler.""" + + SINGLE = "ESPHOME_CORES_SINGLE" + MULTI_NO_ATOMICS = "ESPHOME_CORES_MULTI_NO_ATOMICS" + MULTI_ATOMICS = "ESPHOME_CORES_MULTI_ATOMICS" class PlatformFramework(Enum): @@ -47,6 +57,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 +71,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 +104,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" @@ -368,6 +383,8 @@ CONF_FINGER_ID = "finger_id" CONF_FINGERPRINT_COUNT = "fingerprint_count" CONF_FLASH_LENGTH = "flash_length" CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length" +CONF_FLIP_X = "flip_x" +CONF_FLIP_Y = "flip_y" CONF_FLOW = "flow" CONF_FLOW_CONTROL_PIN = "flow_control_pin" CONF_FONT = "font" @@ -915,6 +932,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 +1205,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..472067797e 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, ) @@ -469,6 +470,52 @@ class Library: return self.as_tuple == other.as_tuple return NotImplemented + def reconcile_with(self, other): + """Merge two libraries, reconciling any conflicts.""" + + if self.name != other.name: + # Different libraries, no reconciliation possible + raise ValueError( + f"Cannot reconcile libraries with different names: {self.name} and {other.name}" + ) + + # repository specificity takes precedence over version specificity + if self.repository is None and other.repository is None: + pass # No repositories, no conflict, continue on + + elif self.repository is None: + # incoming library has a repository, use it + self.repository = other.repository + self.version = other.version + return self + + elif other.repository is None: + return self # use the repository/version already present + + elif self.repository != other.repository: + raise ValueError( + f"Reconciliation failed! Libraries {self} and {other} requested with conflicting repositories!" + ) + + if self.version is None and other.version is None: + return self # Arduino library reconciled against another Arduino library, current is acceptable + + if self.version is None: + # incoming library has a version, use it + self.version = other.version + return self + + if other.version is None: + return self # incoming library has no version, current is acceptable + + # Same versions, current library is acceptable + if self.version != other.version: + raise ValueError( + f"Version pinning failed! Libraries {other} and {self} " + "requested with conflicting versions!" + ) + return self + # pylint: disable=too-many-public-methods class EsphomeCore: @@ -504,8 +551,8 @@ class EsphomeCore: self.main_statements: list[Statement] = [] # A list of statements to insert in the global block (includes and global variables) self.global_statements: list[Statement] = [] - # A set of platformio libraries to add to the project - self.libraries: list[Library] = [] + # A map of platformio libraries to add to the project (shortname: (name, version, repository)) + self.platformio_libraries: dict[str, Library] = {} # A set of build flags to set in the platformio project self.build_flags: set[str] = set() # A set of build unflags to set in the platformio project @@ -549,7 +596,7 @@ class EsphomeCore: self.variables = {} self.main_statements = [] self.global_statements = [] - self.libraries = [] + self.platformio_libraries = {} self.build_flags = set() self.build_unflags = set() self.defines = set() @@ -670,6 +717,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 +737,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) @@ -729,54 +784,22 @@ class EsphomeCore: _LOGGER.debug("Adding global: %s", expression) return expression - def add_library(self, library): + def add_library(self, library: Library): if not isinstance(library, Library): - raise ValueError( + raise TypeError( f"Library {library} must be instance of Library, not {type(library)}" ) - for other in self.libraries[:]: - if other.name is None or library.name is None: - continue - library_name = ( - library.name if "/" not in library.name else library.name.split("/")[1] - ) - other_name = ( - other.name if "/" not in other.name else other.name.split("/")[1] - ) - if other_name != library_name: - continue - if other.repository is not None: - if library.repository is None or other.repository == library.repository: - # Other is using a/the same repository, takes precedence - break - raise ValueError( - f"Adding named Library with repository failed! Libraries {library} and {other} " - "requested with conflicting repositories!" - ) + short_name = ( + library.name if "/" not in library.name else library.name.split("/")[-1] + ) - if library.repository is not None: - # This is more specific since its using a repository - self.libraries.remove(other) - continue - - if library.version is None: - # Other requirement is more specific - break - if other.version is None: - # Found more specific version requirement - self.libraries.remove(other) - continue - if other.version == library.version: - break - - raise ValueError( - f"Version pinning failed! Libraries {library} and {other} " - "requested with conflicting versions!" - ) - else: + if short_name not in self.platformio_libraries: _LOGGER.debug("Adding library: %s", library) - self.libraries.append(library) - return library + self.platformio_libraries[short_name] = library + return library + + self.platformio_libraries[short_name].reconcile_with(library) + return self.platformio_libraries[short_name] def add_build_flag(self, build_flag: str) -> str: self.build_flags.add(build_flag) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index d6fab018cc..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); @@ -309,6 +320,12 @@ void Application::disable_component_loop_(Component *component) { if (this->in_loop_ && i == this->current_loop_index_) { // Decrement so we'll process the swapped component next this->current_loop_index_--; + // Update the loop start time to current time so the swapped component + // gets correct timing instead of inheriting stale timing. + // This prevents integer underflow in timing calculations by ensuring + // the swapped component starts with a fresh timing reference, avoiding + // errors caused by stale or wrapped timing values. + this->loop_component_start_time_ = millis(); } } return; 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 9d863e56cd..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 { @@ -138,7 +141,7 @@ void Component::call_dump_config() { } } } - ESP_LOGE(TAG, " Component %s is marked FAILED: %s", this->get_component_source(), error_msg); + ESP_LOGE(TAG, " %s is marked FAILED: %s", this->get_component_source(), error_msg); } } @@ -191,7 +194,7 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) { return false; } void Component::mark_failed() { - ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source()); + ESP_LOGE(TAG, "%s was marked as failed", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_FAILED; this->status_set_error(); @@ -229,7 +232,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { - ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source()); + ESP_LOGI(TAG, "%s is being reset to construction state", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; // Clear error status when resetting @@ -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 @@ -264,6 +267,7 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } bool Component::is_ready() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || + (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; } bool Component::can_proceed() { return true; } @@ -275,14 +279,14 @@ void Component::status_set_warning(const char *message) { return; this->component_state_ |= STATUS_LED_WARNING; App.app_state_ |= STATUS_LED_WARNING; - ESP_LOGW(TAG, "Component %s set Warning flag: %s", this->get_component_source(), message); + ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message); } void Component::status_set_error(const char *message) { if ((this->component_state_ & STATUS_LED_ERROR) != 0) return; this->component_state_ |= STATUS_LED_ERROR; App.app_state_ |= STATUS_LED_ERROR; - ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message); + ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message); if (strcmp(message, "unspecified") != 0) { // Lazy allocate the error messages vector if needed if (!component_error_messages) { @@ -303,13 +307,13 @@ void Component::status_clear_warning() { if ((this->component_state_ & STATUS_LED_WARNING) == 0) return; this->component_state_ &= ~STATUS_LED_WARNING; - ESP_LOGW(TAG, "Component %s cleared Warning flag", this->get_component_source()); + ESP_LOGW(TAG, "%s cleared Warning flag", this->get_component_source()); } void Component::status_clear_error() { if ((this->component_state_ & STATUS_LED_ERROR) == 0) return; this->component_state_ &= ~STATUS_LED_ERROR; - ESP_LOGE(TAG, "Component %s cleared Error flag", this->get_component_source()); + ESP_LOGE(TAG, "%s cleared Error flag", this->get_component_source()); } void Component::status_momentary_warning(const std::string &name, uint32_t length) { this->status_set_warning(); @@ -395,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); @@ -403,7 +414,7 @@ uint32_t WarnIfComponentBlockingGuard::finish() { } if (should_warn) { const char *src = component_ == nullptr ? "" : component_->get_component_source(); - ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); + ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); ESP_LOGW(TAG, "Components should block for at most 30 ms"); } diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index aab5c2a72d..1e8f670d8b 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -4,6 +4,8 @@ #ifdef USE_API #include "esphome/components/api/api_server.h" +#endif +#ifdef USE_API_SERVICES #include "esphome/components/api/user_services.h" #endif @@ -14,376 +16,189 @@ 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 - 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); - } + +#ifdef USE_API_SERVICES + 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 +#ifdef USE_API_SERVICES bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } #endif #ifdef USE_CAMERA diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index eda786be7f..7a9771b8f2 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -10,7 +10,7 @@ namespace esphome { -#ifdef USE_API +#ifdef USE_API_SERVICES namespace api { class UserServiceDescriptor; } // namespace api @@ -45,7 +45,7 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; #endif -#ifdef USE_API +#ifdef USE_API_SERVICES virtual bool on_service(api::UserServiceDescriptor *service); #endif #ifdef USE_CAMERA @@ -122,7 +122,7 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR TEXT_SENSOR, #endif -#ifdef USE_API +#ifdef USE_API_SERVICES SERVICE, #endif #ifdef USE_CAMERA @@ -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 aed37d31d7..9355d56084 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -15,6 +15,9 @@ #define ESPHOME_VARIANT "ESP32" #define ESPHOME_DEBUG_SCHEDULER +// Default threading model for static analysis (ESP32 is multi-core with atomics) +#define ESPHOME_CORES_MULTI_ATOMICS + // logger #define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE @@ -108,7 +111,7 @@ #define USE_API_CLIENT_DISCONNECTED_TRIGGER #define USE_API_NOISE #define USE_API_PLAINTEXT -#define USE_API_YAML_SERVICES +#define USE_API_SERVICES #define USE_MD5 #define USE_MQTT #define USE_NETWORK @@ -145,6 +148,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 a3244856a2..cc388ffb4c 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -187,14 +187,23 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy # No name to validate return config + # Skip validation for internal entities + # Internal entities are not exposed to Home Assistant and don't use the hash-based + # entity state tracking system, so name collisions don't matter for them + if config.get(CONF_INTERNAL, False): + return config + # Get the entity name entity_name = config[CONF_NAME] # 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 @@ -203,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.cpp b/esphome/core/helpers.cpp index 0c11c5d486..e84f5a7317 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -258,12 +258,14 @@ std::string format_hex(const uint8_t *data, size_t length) { std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } -std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) { + +// Shared implementation for uint8_t and string hex formatting +static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { if (data == nullptr || length == 0) return ""; std::string ret; uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise - ret.resize(multiple * length - 1); + ret.resize(multiple * length - (separator ? 1 : 0)); for (size_t i = 0; i < length; i++) { ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4); ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F); @@ -274,6 +276,10 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator return ret + " (" + std::to_string(length) + ")"; return ret; } + +std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) { + return format_hex_pretty_uint8(data, length, separator, show_length); +} std::string format_hex_pretty(const std::vector &data, char separator, bool show_length) { return format_hex_pretty(data.data(), data.size(), separator, show_length); } @@ -283,7 +289,7 @@ std::string format_hex_pretty(const uint16_t *data, size_t length, char separato return ""; std::string ret; uint8_t multiple = separator ? 5 : 4; // 5 if separator is not \0, 4 otherwise - ret.resize(multiple * length - 1); + ret.resize(multiple * length - (separator ? 1 : 0)); for (size_t i = 0; i < length; i++) { ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12); ret[multiple * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8); @@ -300,20 +306,7 @@ std::string format_hex_pretty(const std::vector &data, char separator, return format_hex_pretty(data.data(), data.size(), separator, show_length); } std::string format_hex_pretty(const std::string &data, char separator, bool show_length) { - if (data.empty()) - return ""; - std::string ret; - uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise - ret.resize(multiple * data.length() - 1); - for (size_t i = 0; i < data.length(); i++) { - ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4); - ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F); - if (separator && i != data.length() - 1) - ret[multiple * i + 2] = separator; - } - if (show_length && data.length() > 4) - return ret + " (" + std::to_string(data.length()) + ")"; - return ret; + return format_hex_pretty_uint8(reinterpret_cast(data.data()), data.length(), separator, show_length); } std::string format_bin(const uint8_t *data, size_t length) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 58f162ff9d..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 @@ -783,7 +801,7 @@ template class RAMAllocator { T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); } T *reallocate(T *p, size_t n, size_t manual_size) { - size_t size = n * sizeof(T); + size_t size = n * manual_size; T *ptr = nullptr; #ifdef USE_ESP32 if (this->flags_ & Flags::ALLOC_EXTERNAL) { 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 d3da003a88..9e66fd3432 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 @@ -51,7 +54,7 @@ static void validate_static_string(const char *name) { ESP_LOGW(TAG, "WARNING: Scheduler name '%s' at %p might be on heap (static ref at %p)", name, name, static_str); } } -#endif +#endif /* ESPHOME_DEBUG_SCHEDULER */ // A note on locking: the `lock_` lock protects the `items_` and `to_add_` containers. It must be taken when writing to // them (i.e. when adding/removing items, but not when changing items). As items are only deleted from the loop task, @@ -66,10 +69,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type if (delay == SCHEDULER_DONT_RUN) { // Still need to cancel existing timer if name is not empty - if (this->is_name_valid_(name_cstr)) { - LockGuard guard{this->lock_}; - this->cancel_item_locked_(component, name_cstr, type); - } + LockGuard guard{this->lock_}; + this->cancel_item_locked_(component, name_cstr, type); return; } @@ -81,9 +82,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->callback = std::move(func); item->remove = false; -#if !defined(USE_ESP8266) && !defined(USE_RP2040) +#ifndef ESPHOME_CORES_SINGLE // Special handling for defer() (delay = 0, type = TIMEOUT) - // ESP8266 and RP2040 are excluded because they don't need thread-safe defer handling + // Single-core platforms don't need thread-safe defer handling if (delay == 0 && type == SchedulerItem::TIMEOUT) { // Put in defer queue for guaranteed FIFO execution LockGuard guard{this->lock_}; @@ -91,9 +92,10 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type this->defer_queue_.push_back(std::move(item)); return; } -#endif +#endif /* not ESPHOME_CORES_SINGLE */ - 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) { @@ -121,14 +123,12 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(), name_cstr ? name_cstr : "(null)", type_str, delay, static_cast(item->next_execution_ - now)); } -#endif +#endif /* ESPHOME_DEBUG_SCHEDULER */ LockGuard guard{this->lock_}; // If name is provided, do atomic cancel-and-add - if (this->is_name_valid_(name_cstr)) { - // Cancel existing items - this->cancel_item_locked_(component, name_cstr, type); - } + // Cancel existing items + this->cancel_item_locked_(component, name_cstr, type); // Add new item directly to to_add_ // since we have the lock held this->to_add_.push_back(std::move(item)); @@ -197,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; } @@ -219,20 +217,24 @@ 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 + // It performs cleanup 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_()) + + // If no items, return empty optional + if (this->cleanup_() == 0) 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() { -#if !defined(USE_ESP8266) && !defined(USE_RP2040) +void HOT Scheduler::call(uint32_t now) { +#ifndef ESPHOME_CORES_SINGLE // Process defer queue first to guarantee FIFO execution order for deferred items. // Previously, defer() used the heap which gave undefined order for equal timestamps, // causing race conditions on multi-core systems (ESP32, BK7200). @@ -240,8 +242,7 @@ void HOT Scheduler::call() { // - Deferred items (delay=0) go directly to defer_queue_ in set_timer_common_ // - Items execute in exact order they were deferred (FIFO guarantee) // - No deferred items exist in to_add_, so processing order doesn't affect correctness - // ESP8266 and RP2040 don't use this queue - they fall back to the heap-based approach - // (ESP8266: single-core, RP2040: empty mutex implementation). + // Single-core platforms don't use this queue and fall back to the heap-based approach. // // Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still // processed here. They are removed from the queue normally via pop_front() but skipped @@ -260,23 +261,33 @@ 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 +#endif /* not ESPHOME_CORES_SINGLE */ - 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_); - while (!this->empty_()) { +#ifdef ESPHOME_CORES_MULTI_ATOMICS + const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); + const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, + major_dbg, last_dbg); +#else /* not ESPHOME_CORES_MULTI_ATOMICS */ + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, + this->millis_major_, this->last_millis_); +#endif /* else ESPHOME_CORES_MULTI_ATOMICS */ + // Cleanup before debug output + this->cleanup_(); + while (!this->items_.empty()) { std::unique_ptr item; { LockGuard guard{this->lock_}; @@ -287,7 +298,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)); } @@ -300,7 +311,7 @@ void HOT Scheduler::call() { std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); } } -#endif // ESPHOME_DEBUG_SCHEDULER +#endif /* ESPHOME_DEBUG_SCHEDULER */ // If we have too many items to remove if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { @@ -327,12 +338,14 @@ void HOT Scheduler::call() { this->to_remove_ = 0; } - while (!this->empty_()) { + // Cleanup removed items before processing + this->cleanup_(); + while (!this->items_.empty()) { // use scoping to indicate visibility of `item` variable { // Don't copy-by value yet auto &item = this->items_[0]; - if (item->next_execution_ > now) { + if (item->next_execution_ > now_64) { // Not reached timeout yet, done for this call break; } @@ -346,13 +359,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); -#endif + item->next_execution_, now_64); +#endif /* ESPHOME_DEBUG_SCHEDULER */ // Warning: During callback(), a lot of stuff can happen, including: // - timeouts/intervals get added, potentially invalidating vector pointers // - timeouts/intervals get cancelled - this->execute_item_(item.get()); + this->execute_item_(item.get(), now); } { @@ -371,7 +384,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)); @@ -393,8 +406,8 @@ void HOT Scheduler::process_to_add() { } this->to_add_.clear(); } -void HOT Scheduler::cleanup_() { - // Fast path: if nothing to remove, just return +size_t HOT Scheduler::cleanup_() { + // Fast path: if nothing to remove, just return the current size // Reading to_remove_ without lock is safe because: // 1. We only call this from the main thread during call() // 2. If it's 0, there's definitely nothing to cleanup @@ -402,7 +415,7 @@ void HOT Scheduler::cleanup_() { // 4. Not all platforms support atomics, so we accept this race in favor of performance // 5. The worst case is a one-loop-iteration delay in cleanup, which is harmless if (this->to_remove_ == 0) - return; + return this->items_.size(); // We must hold the lock for the entire cleanup operation because: // 1. We're modifying items_ (via pop_raw_) which requires exclusive access @@ -416,10 +429,11 @@ void HOT Scheduler::cleanup_() { while (!this->items_.empty()) { auto &item = this->items_[0]; if (!item->remove) - return; + break; this->to_remove_--; this->pop_raw_(); } + return this->items_.size(); } void HOT Scheduler::pop_raw_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); @@ -427,11 +441,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(); } @@ -442,10 +454,6 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co // Get the name as const char* const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); - // Handle null or empty names - if (!this->is_name_valid_(name_cstr)) - return false; - // obtain lock because this function iterates and can be called from non-loop task context LockGuard guard{this->lock_}; return this->cancel_item_locked_(component, name_cstr, type); @@ -453,10 +461,15 @@ 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) { + return false; + } + size_t total_cancelled = 0; // Check all containers for matching items -#if !defined(USE_ESP8266) && !defined(USE_RP2040) +#ifndef ESPHOME_CORES_SINGLE // Only check defer queue for timeouts (intervals never go there) if (type == SchedulerItem::TIMEOUT) { for (auto &item : this->defer_queue_) { @@ -466,7 +479,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c } } } -#endif +#endif /* not ESPHOME_CORES_SINGLE */ // Cancel items in the main heap for (auto &item : this->items_) { @@ -489,19 +502,162 @@ 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) +uint64_t Scheduler::millis_64_(uint32_t now) { + // THREAD SAFETY NOTE: + // This function has three implementations, based on the precompiler flags + // - ESPHOME_CORES_SINGLE - Runs on single-core platforms (ESP8266, RP2040, etc.) + // - ESPHOME_CORES_MULTI_NO_ATOMICS - Runs on multi-core platforms without atomics (LibreTiny) + // - ESPHOME_CORES_MULTI_ATOMICS - Runs on multi-core platforms with atomics (ESP32, HOST, etc.) + // + // Make sure all changes are synchronized if you edit this function. + // + // 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. + // + +#ifdef ESPHOME_CORES_SINGLE + // This is the single core implementation. + // + // Single-core platforms have no concurrency, so this is a simple implementation + // that just tracks 32-bit rollover (every 49.7 days) without any locking or atomics. + + uint16_t major = this->millis_major_; + uint32_t last = this->last_millis_; + + // Check for rollover + if (now < last && (last - now) > HALF_MAX_UINT32) { this->millis_major_++; - ESP_LOGD(TAG, "Incrementing scheduler major at %" PRIu64 "ms", - now + (static_cast(this->millis_major_) << 32)); + major++; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif /* ESPHOME_DEBUG_SCHEDULER */ } - this->last_millis_ = now; + + // Only update if time moved forward + if (now > last) { + this->last_millis_ = now; + } + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time - return now + (static_cast(this->millis_major_) << 32); + return now + (static_cast(major) << 32); + +#elif defined(ESPHOME_CORES_MULTI_NO_ATOMICS) + // This is the multi core no atomics implementation. + // + // Without atomics, this implementation uses locks more aggressively: + // 1. Always locks when near the rollover boundary (within 10 seconds) + // 2. Always locks when detecting a large backwards jump + // 3. Updates without lock in normal forward progression (accepting minor races) + // This is less efficient but necessary without atomic operations. + uint16_t major = this->millis_major_; + 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_++; + major++; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } + // 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; + } + // If now <= last and we're not near rollover, don't update + // This minimizes backwards time movement + + // Combine major (high 32 bits) and now (low 32 bits) into 64-bit time + return now + (static_cast(major) << 32); + +#elif defined(ESPHOME_CORES_MULTI_ATOMICS) + // This is the multi core with atomics implementation. + // + // Uses atomic operations with acquire/release semantics to ensure coherent + // reads of millis_major_ and last_millis_ across cores. Features: + // 1. Epoch-coherency retry loop to handle concurrent updates + // 2. Lock only taken for actual rollover detection and update + // 3. Lock-free CAS updates for normal forward time progression + // 4. Memory ordering ensures cores see consistent time values + + for (;;) { + uint16_t major = this->millis_major_.load(std::memory_order_acquire); + + /* + * Acquire so that if we later decide **not** to take the lock we still + * observe a `millis_major_` value coherent with the loaded `last_millis_`. + * The acquire load ensures any later read of `millis_major_` sees its + * corresponding increment. + */ + uint32_t last = this->last_millis_.load(std::memory_order_acquire); + + // 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; mutex already provides ordering + 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_.fetch_add(1, std::memory_order_relaxed); + major++; +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } + /* + * Update last_millis_ while holding the lock to prevent races + * Publish the new low-word *after* bumping `millis_major_` (done above) + * so readers never see a mismatched pair. + */ + this->last_millis_.store(now, std::memory_order_release); + } 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_release, // success + std::memory_order_relaxed)) { // failure + break; + } + // CAS failure means no data was published; relaxed is fine + // last is automatically updated by compare_exchange_weak if it fails + } + } + uint16_t major_end = this->millis_major_.load(std::memory_order_relaxed); + if (major_end == major) + return now + (static_cast(major) << 32); + } + // Unreachable - the loop always returns when major_end == major + __builtin_unreachable(); + +#else +#error \ + "No platform threading model defined. One of ESPHOME_CORES_SINGLE, ESPHOME_CORES_MULTI_NO_ATOMICS, or ESPHOME_CORES_MULTI_ATOMICS must be defined." +#endif } bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 084ff699c5..c14b7debe4 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -1,9 +1,13 @@ #pragma once +#include "esphome/core/defines.h" #include #include #include #include +#ifdef ESPHOME_CORES_MULTI_ATOMICS +#include +#endif #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -52,9 +56,16 @@ 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 + // Returns the time in milliseconds until the next scheduled item, or nullopt if no items + // This method performs cleanup of removed items before checking the schedule + // IMPORTANT: This method should only be called from the main thread (loop task). + 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 +125,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,8 +149,11 @@ 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_(); - void cleanup_(); + uint64_t millis_64_(uint32_t now); + // Cleanup logically deleted items from the scheduler + // Returns the number of items remaining after cleanup + // IMPORTANT: This method should only be called from the main thread (loop task). + size_t cleanup_(); void pop_raw_(); private: @@ -150,9 +165,6 @@ class Scheduler { return is_static_string ? static_cast(name_ptr) : static_cast(name_ptr)->c_str(); } - // Helper to check if a name is valid (not null and not empty) - inline bool is_name_valid_(const char *name) { return name != nullptr && name[0] != '\0'; } - // Common implementation for cancel operations bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); @@ -178,37 +190,50 @@ 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 { return item->remove || (item->component != nullptr && item->component->is_failed()); } - // Check if the scheduler has no items. - // IMPORTANT: This method should only be called from the main thread (loop task). - // It performs cleanup of removed items and checks if the queue is empty. - // The items_.empty() check at the end is done without a lock for performance, - // which is safe because this is only called from the main thread while other - // threads only add items (never remove them). - bool empty_() { - this->cleanup_(); - return this->items_.empty(); - } - Mutex lock_; std::vector> items_; std::vector> to_add_; -#if !defined(USE_ESP8266) && !defined(USE_RP2040) - // ESP8266 and RP2040 don't need the defer queue because: - // ESP8266: Single-core with no preemptive multitasking - // RP2040: Currently has empty mutex implementation in ESPHome - // Both platforms save 40 bytes of RAM by excluding this +#ifndef ESPHOME_CORES_SINGLE + // Single-core platforms don't need the defer queue and save 40 bytes of RAM std::deque> defer_queue_; // FIFO queue for defer() calls -#endif - uint32_t last_millis_{0}; - uint16_t millis_major_{0}; +#endif /* ESPHOME_CORES_SINGLE */ uint32_t to_remove_{0}; + +#ifdef ESPHOME_CORES_MULTI_ATOMICS + /* + * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates + * + * MEMORY-ORDERING NOTE + * -------------------- + * `last_millis_` and `millis_major_` form a single 64-bit timestamp split in half. + * Writers publish `last_millis_` with memory_order_release and readers use + * memory_order_acquire. This ensures that once a reader sees the new low word, + * it also observes the corresponding increment of `millis_major_`. + */ + std::atomic last_millis_{0}; +#else /* not ESPHOME_CORES_MULTI_ATOMICS */ + // Platforms without atomic support or single-threaded platforms + uint32_t last_millis_{0}; +#endif /* else ESPHOME_CORES_MULTI_ATOMICS */ + + /* + * Upper 16 bits of the 64-bit millis counter. Incremented only while holding + * `lock_`; read concurrently. Atomic (relaxed) avoids a formal data race. + * Ordering relative to `last_millis_` is provided by its release store and the + * corresponding acquire loads. + */ +#ifdef ESPHOME_CORES_MULTI_ATOMICS + std::atomic millis_major_{0}; +#else /* not ESPHOME_CORES_MULTI_ATOMICS */ + uint16_t millis_major_{0}; +#endif /* else ESPHOME_CORES_MULTI_ATOMICS */ }; } // namespace esphome 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/platformio_api.py b/esphome/platformio_api.py index 808db03231..e34ac028f8 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -78,6 +78,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int: os.environ.setdefault( "PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path()) ) + # Suppress Python syntax warnings from third-party scripts during compilation + os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") cmd = ["platformio"] + list(args) if not CORE.verbose: 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/esphome/wizard.py b/esphome/wizard.py index 1826487aa4..6f7cbd1ff4 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -411,7 +411,7 @@ def wizard(path): safe_print("Options:") for board_id, board_data in boards_list: safe_print(f" - {board_id} - {board_data['name']}") - boards.append(board_id) + boards.append(board_id.lower()) while True: board = safe_input(color(AnsiFore.BOLD_WHITE, "(board): ")) diff --git a/esphome/writer.py b/esphome/writer.py index 943dfa78cc..d2ec112ec8 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -7,7 +7,6 @@ import re from esphome import loader from esphome.config import iter_component_configs, iter_components from esphome.const import ( - ENV_NOGITIGNORE, HEADER_FILE_EXTENSIONS, PLATFORM_ESP32, SOURCE_FILE_EXTENSIONS, @@ -16,8 +15,6 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import ( copy_file_if_changed, - get_bool_env, - mkdir_p, read_file, walk_files, write_file_if_changed, @@ -30,8 +27,6 @@ CPP_AUTO_GENERATE_BEGIN = "// ========== AUTO GENERATED CODE BEGIN ===========" CPP_AUTO_GENERATE_END = "// =========== AUTO GENERATED CODE END ============" CPP_INCLUDE_BEGIN = "// ========== AUTO GENERATED INCLUDE BLOCK BEGIN ===========" CPP_INCLUDE_END = "// ========== AUTO GENERATED INCLUDE BLOCK END ===========" -INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ===========" -INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============" CPP_BASE_FORMAT = ( """// Auto generated code by esphome @@ -50,20 +45,6 @@ void loop() { """, ) -INI_BASE_FORMAT = ( - """; Auto generated code by esphome - -[common] -lib_deps = -build_flags = -upload_flags = - -""", - """ - -""", -) - UPLOAD_SPEED_OVERRIDE = { "esp210": 57600, } @@ -140,37 +121,6 @@ def update_storage_json(): new.save(path) -def format_ini(data: dict[str, str | list[str]]) -> str: - content = "" - for key, value in sorted(data.items()): - if isinstance(value, list): - content += f"{key} =\n" - for x in value: - content += f" {x}\n" - else: - content += f"{key} = {value}\n" - return content - - -def get_ini_content(): - CORE.add_platformio_option( - "lib_deps", [x.as_lib_dep for x in CORE.libraries] + ["${common.lib_deps}"] - ) - # Sort to avoid changing build flags order - CORE.add_platformio_option("build_flags", sorted(CORE.build_flags)) - - # Sort to avoid changing build unflags order - CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) - - content = "[platformio]\n" - content += f"description = ESPHome {__version__}\n" - - content += f"[env:{CORE.name}]\n" - content += format_ini(CORE.platformio_options) - - return content - - def find_begin_end(text, begin_s, end_s): begin_index = text.find(begin_s) if begin_index == -1: @@ -198,31 +148,6 @@ def find_begin_end(text, begin_s, end_s): return text[:begin_index], text[(end_index + len(end_s)) :] -def write_platformio_ini(content): - update_storage_json() - path = CORE.relative_build_path("platformio.ini") - - if os.path.isfile(path): - text = read_file(path) - content_format = find_begin_end( - text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END - ) - else: - content_format = INI_BASE_FORMAT - full_file = f"{content_format[0] + INI_AUTO_GENERATE_BEGIN}\n{content}" - full_file += INI_AUTO_GENERATE_END + content_format[1] - write_file_if_changed(path, full_file) - - -def write_platformio_project(): - mkdir_p(CORE.build_path) - - content = get_ini_content() - if not get_bool_env(ENV_NOGITIGNORE): - write_gitignore() - write_platformio_ini(content) - - DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ #pragma once #include "esphome/core/macros.h" diff --git a/platformio.ini b/platformio.ini index 41deb19837..350f220853 100644 --- a/platformio.ini +++ b/platformio.ini @@ -35,7 +35,7 @@ build_flags = lib_deps = esphome/noise-c@0.1.10 ; api improv/Improv@1.2.4 ; improv_serial / esp32_improv - bblanchon/ArduinoJson@6.18.5 ; json + bblanchon/ArduinoJson@7.4.2 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 pavlodn/HaierProtocol@0.9.31 ; haier @@ -61,6 +61,7 @@ src_filter = +<../tests/dummy_main.cpp> +<../.temp/all-include.cpp> lib_ldf_mode = off +lib_compat_mode = strict ; This are common settings for all Arduino-framework based environments. [common:arduino] @@ -137,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) @@ -211,6 +212,7 @@ build_unflags = extends = common:arduino platform = libretiny@1.9.1 framework = arduino +lib_compat_mode = soft lib_deps = droscy/esp_wireguard@0.4.2 ; wireguard build_flags = @@ -233,7 +235,7 @@ build_flags = -DUSE_ZEPHYR -DUSE_NRF52 lib_deps = - bblanchon/ArduinoJson@7.0.0 ; json + bblanchon/ArduinoJson@7.4.2 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code pavlodn/HaierProtocol@0.9.31 ; haier functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 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 a6bcebaeea..cc69186e49 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==34.1.0 +aioesphomeapi==37.0.4 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 ef1fc4f2d6..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.2 # 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 df1f3f8caa..ad6c3c3ed2 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 @@ -249,6 +248,22 @@ class TypeInfo(ABC): return 4 # 28 bits return 5 # 32 bits (maximum for uint32_t) + def _get_simple_size_calculation( + self, name: str, force: bool, base_method: str, value_expr: str = None + ) -> str: + """Helper for simple size calculations. + + Args: + name: Field name + force: Whether this is for a repeated field + base_method: Base method name (e.g., "add_int32_field") + value_expr: Optional value expression (defaults to name) + """ + field_id_size = self.calculate_field_id_size() + method = f"{base_method}_repeated" if force else base_method + value = value_expr if value_expr else name + return f"ProtoSize::{method}(total_size, {field_id_size}, {value});" + @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: """Calculate the size needed for encoding this field. @@ -258,6 +273,14 @@ class TypeInfo(ABC): force: Whether to force encoding the field even if it has a default value """ + def get_fixed_size_bytes(self) -> int | None: + """Get the number of bytes for fixed-size fields (float, double, fixed32, etc). + + Returns: + The number of bytes (4 or 8) for fixed-size fields, None for variable-size fields. + """ + return None + @abstractmethod def get_estimated_size(self) -> int: """Get estimated size in bytes for this field with typical values. @@ -269,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.""" @@ -296,8 +355,10 @@ class DoubleType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_fixed_field<8>(total_size, {field_id_size}, {name} != 0.0, {force_str(force)});" - return o + return f"ProtoSize::add_double_field(total_size, {field_id_size}, {name});" + + def get_fixed_size_bytes(self) -> int: + return 8 def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes for double @@ -318,8 +379,10 @@ class FloatType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_fixed_field<4>(total_size, {field_id_size}, {name} != 0.0f, {force_str(force)});" - return o + return f"ProtoSize::add_float_field(total_size, {field_id_size}, {name});" + + def get_fixed_size_bytes(self) -> int: + return 4 def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 4 # field ID + 4 bytes for float @@ -339,9 +402,7 @@ class Int64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_int64_field(total_size, {field_id_size}, {name}, {force_str(force)});" - return o + return self._get_simple_size_calculation(name, force, "add_int64_field") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -361,9 +422,7 @@ class UInt64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_uint64_field(total_size, {field_id_size}, {name}, {force_str(force)});" - return o + return self._get_simple_size_calculation(name, force, "add_uint64_field") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -383,9 +442,7 @@ class Int32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_int32_field(total_size, {field_id_size}, {name}, {force_str(force)});" - return o + return self._get_simple_size_calculation(name, force, "add_int32_field") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -406,8 +463,10 @@ class Fixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_fixed_field<8>(total_size, {field_id_size}, {name} != 0, {force_str(force)});" - return o + return f"ProtoSize::add_fixed64_field(total_size, {field_id_size}, {name});" + + def get_fixed_size_bytes(self) -> int: + return 8 def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes fixed @@ -428,8 +487,10 @@ class Fixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_fixed_field<4>(total_size, {field_id_size}, {name} != 0, {force_str(force)});" - return o + return f"ProtoSize::add_fixed32_field(total_size, {field_id_size}, {name});" + + def get_fixed_size_bytes(self) -> int: + return 4 def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 4 # field ID + 4 bytes fixed @@ -448,9 +509,7 @@ class BoolType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_bool_field(total_size, {field_id_size}, {name}, {force_str(force)});" - return o + return self._get_simple_size_calculation(name, force, "add_bool_field") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 1 # field ID + 1 byte @@ -471,9 +530,7 @@ class StringType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_string_field(total_size, {field_id_size}, {name}, {force_str(force)});" - return o + return self._get_simple_size_calculation(name, force, "add_string_field") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string @@ -498,20 +555,27 @@ class MessageType(TypeInfo): @property def encode_func(self) -> str: - return f"encode_message<{self.cpp_type}>" + return "encode_message" @property def decode_length(self) -> str: - return f"value.as_message<{self.cpp_type}>()" + # Override to return None for message types because we can't use template-based + # decoding when the specific message type isn't known at compile time. + # Instead, we use the non-template decode_to_message() method which allows + # runtime polymorphism through virtual function calls. + return None + + @property + def decode_length_content(self) -> str: + # Custom decode that doesn't use templates + 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);" return o def get_size_calculation(self, name: str, force: bool = False) -> str: - field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_message_object(total_size, {field_id_size}, {name}, {force_str(force)});" - return o + return self._get_simple_size_calculation(name, force, "add_message_object") def get_estimated_size(self) -> int: return ( @@ -538,14 +602,91 @@ class BytesType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_string_field(total_size, {field_id_size}, {name}, {force_str(force)});" - return o + return self._get_simple_size_calculation(name, force, "add_string_field") def get_estimated_size(self) -> int: 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" @@ -560,9 +701,7 @@ class UInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_uint32_field(total_size, {field_id_size}, {name}, {force_str(force)});" - return o + return self._get_simple_size_calculation(name, force, "add_uint32_field") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -576,23 +715,27 @@ class EnumType(TypeInfo): @property def decode_varint(self) -> str: - return f"value.as_enum<{self.cpp_type}>()" + return f"static_cast<{self.cpp_type}>(value.as_uint32())" default_value = "" wire_type = WireType.VARINT # Uses wire type 0 @property def encode_func(self) -> str: - return f"encode_enum<{self.cpp_type}>" + return "encode_uint32" + + @property + def encode_content(self) -> str: + return f"buffer.{self.encode_func}({self.number}, static_cast(this->{self.field_name}));" def dump(self, name: str) -> str: o = f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));" return o def get_size_calculation(self, name: str, force: bool = False) -> str: - field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_enum_field(total_size, {field_id_size}, static_cast({name}), {force_str(force)});" - return o + return self._get_simple_size_calculation( + name, force, "add_enum_field", f"static_cast({name})" + ) def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 1 # field ID + 1 byte typical enum @@ -613,8 +756,10 @@ class SFixed32Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_fixed_field<4>(total_size, {field_id_size}, {name} != 0, {force_str(force)});" - return o + return f"ProtoSize::add_sfixed32_field(total_size, {field_id_size}, {name});" + + def get_fixed_size_bytes(self) -> int: + return 4 def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 4 # field ID + 4 bytes fixed @@ -635,8 +780,10 @@ class SFixed64Type(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_fixed_field<8>(total_size, {field_id_size}, {name} != 0, {force_str(force)});" - return o + return f"ProtoSize::add_sfixed64_field(total_size, {field_id_size}, {name});" + + def get_fixed_size_bytes(self) -> int: + return 8 def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes fixed @@ -656,9 +803,7 @@ class SInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_sint32_field(total_size, {field_id_size}, {name}, {force_str(force)});" - return o + return self._get_simple_size_calculation(name, force, "add_sint32_field") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -678,9 +823,7 @@ class SInt64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - field_id_size = self.calculate_field_id_size() - o = f"ProtoSize::add_sint64_field(total_size, {field_id_size}, {name}, {force_str(force)});" - return o + return self._get_simple_size_calculation(name, force, "add_sint64_field") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -689,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 @@ -716,25 +870,20 @@ 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 def decode_length_content(self) -> str: content = self._ti.decode_length + if content is None and isinstance(self._ti, MessageType): + # Special handling for non-template message decoding + 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 @@ -742,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 @@ -755,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 @@ -771,7 +912,10 @@ class RepeatedTypeInfo(TypeInfo): @property def encode_content(self) -> str: o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" - o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" + if isinstance(self._ti, EnumType): + o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" + else: + o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" o += "}" return o @@ -795,11 +939,23 @@ class RepeatedTypeInfo(TypeInfo): field_id_size = self._ti.calculate_field_id_size() o = f"ProtoSize::add_repeated_message(total_size, {field_id_size}, {name});" return o + # For other repeated types, use the underlying type's size calculation with force=True o = f"if (!{name}.empty()) {{\n" - o += f" for (const auto {'' if self._ti_is_bool else '&'}it : {name}) {{\n" - o += f" {self._ti.get_size_calculation('it', True)}\n" - o += " }\n" + + # Check if this is a fixed-size type by seeing if it has a fixed byte count + num_bytes = self._ti.get_fixed_size_bytes() + if num_bytes is not None: + # Fixed types have constant size per element, so we can multiply + field_id_size = self._ti.calculate_field_id_size() + # Pre-calculate the total bytes per element + bytes_per_element = field_id_size + num_bytes + o += f" total_size += {name}.size() * {bytes_per_element};\n" + else: + # Other types need the actual value + o += f" for (const auto {'' if self._ti_is_bool else '&'}it : {name}) {{\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" + o += " }\n" o += "}" return o @@ -815,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], set[str]]: """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, used_messages) """ 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[ @@ -831,6 +988,7 @@ def build_type_usage_map( message_usage: dict[ str, set[str] ] = {} # message_name -> set of message names that use it + used_messages: set[str] = set() # Track which messages are actually used # Build message name to ifdef mapping for quick lookup message_to_ifdef: dict[str, str | None] = { @@ -838,18 +996,35 @@ def build_type_usage_map( } # Analyze field usage + # Also track field_ifdef for message types + message_field_ifdefs: dict[ + str, set[str | None] + ] = {} # message_name -> set of field_ifdefs that use it + for message in file_desc.message_type: + # Skip deprecated messages entirely + if message.options.deprecated: + continue + for field in message.field: + # Skip deprecated fields when tracking enum usage + if field.options.deprecated: + continue + type_name = field.type_name.split(".")[-1] if field.type_name else None if not type_name: continue - # Track enum usage + # Track enum usage (only from non-deprecated fields) if field.type == 14: # TYPE_ENUM enum_usage.setdefault(type_name, set()).add(message.name) # Track message usage elif field.type == 11: # TYPE_MESSAGE message_usage.setdefault(type_name, set()).add(message.name) + # Also track the field_ifdef if present + field_ifdef = get_field_opt(field, pb.field_ifdef) + message_field_ifdefs.setdefault(type_name, set()).add(field_ifdef) + used_messages.add(type_name) # Helper to get unique ifdef from a set of messages def get_unique_ifdef(message_names: set[str]) -> str | None: @@ -875,9 +1050,16 @@ def build_type_usage_map( message_ifdef_map[message.name] = explicit_ifdef elif message.name in message_usage: # Inherit ifdef if all parent messages have the same one - message_ifdef_map[message.name] = get_unique_ifdef( - message_usage[message.name] - ) + if parent_ifdef := get_unique_ifdef(message_usage[message.name]): + message_ifdef_map[message.name] = parent_ifdef + elif message.name in message_field_ifdefs: + # If no parent message ifdef, check if all fields using this message have the same field_ifdef + field_ifdefs = message_field_ifdefs[message.name] - {None} + message_ifdef_map[message.name] = ( + field_ifdefs.pop() if len(field_ifdefs) == 1 else None + ) + else: + message_ifdef_map[message.name] = None else: message_ifdef_map[message.name] = None @@ -909,7 +1091,55 @@ 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: + # Skip deprecated messages + if msg.options.deprecated: + continue + + 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 + # Service messages are always used + used_messages.add(msg.name) + + # 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, + used_messages, + ) def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]: @@ -951,10 +1181,11 @@ 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) + # Skip deprecated fields + if field.options.deprecated: + continue + + ti = create_field_type_info(field) # Add estimated size for this field total_size += ti.get_estimated_size() @@ -964,7 +1195,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] = [] @@ -985,15 +1217,31 @@ def build_message_type( # Get message ID if it's a service message message_id: int | None = get_opt(desc, pb.id) + # Get source direction to determine if we need decode/encode methods + source = message_source_map[desc.name] + needs_decode = source in (SOURCE_BOTH, SOURCE_CLIENT) + needs_encode = source in (SOURCE_BOTH, SOURCE_SERVER) + # Add MESSAGE_TYPE method if this is a service message if message_id is not None: + # Validate that message_id fits in uint8_t + if message_id > 255: + raise ValueError( + f"Message ID {message_id} for {desc.name} exceeds uint8_t maximum (255)" + ) + # Add static constexpr for message type - public_content.append(f"static constexpr uint16_t MESSAGE_TYPE = {message_id};") + public_content.append(f"static constexpr uint8_t MESSAGE_TYPE = {message_id};") # Add estimated size constant estimated_size = calculate_message_estimated_size(desc) + # Validate that estimated_size fits in uint8_t + if estimated_size > 255: + raise ValueError( + f"Estimated size {estimated_size} for {desc.name} exceeds uint8_t maximum (255)" + ) public_content.append( - f"static constexpr uint16_t ESTIMATED_SIZE = {estimated_size};" + f"static constexpr uint8_t ESTIMATED_SIZE = {estimated_size};" ) # Add message_name method inline in header @@ -1005,76 +1253,120 @@ 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) + # Skip deprecated fields completely + if field.options.deprecated: + continue + + 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] - # Always include encode/decode logic for all fields - encode.append(ti.encode_content) - size_calc.append(ti.get_size_calculation(f"this->{ti.field_name}")) + 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)) - if ti.decode_varint_content: - decode_varint.append(ti.decode_varint_content) - if ti.decode_length_content: - decode_length.append(ti.decode_length_content) - if ti.decode_32bit_content: - decode_32bit.append(ti.decode_32bit_content) - if ti.decode_64bit_content: - decode_64bit.append(ti.decode_64bit_content) + # Only collect encode logic if this message needs it + if needs_encode: + # 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.extend( + wrap_with_ifdef(ti.decode_varint_content, field_ifdef) + ) + if ti.decode_length_content: + decode_length.extend( + wrap_with_ifdef(ti.decode_length_content, field_ifdef) + ) + if ti.decode_32bit_content: + decode_32bit.extend( + wrap_with_ifdef(ti.decode_32bit_content, field_ifdef) + ) + if 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;" protected_content.insert(0, prot) - # Only generate encode method if there are fields to encode - if encode: + # Only generate encode method if this message needs encoding and has fields + if needs_encode and encode: o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{" if len(encode) == 1 and len(encode[0]) + len(o) + 3 < 120: o += f" {encode[0]} " @@ -1085,10 +1377,10 @@ def build_message_type( cpp += o prot = "void encode(ProtoWriteBuffer buffer) const override;" public_content.append(prot) - # If no fields to encode, the default implementation in ProtoMessage will be used + # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used - # Add calculate_size method only if there are fields - if size_calc: + # Add calculate_size method only if this message needs encoding and has fields + if needs_encode and size_calc: o = f"void {desc.name}::calculate_size(uint32_t &total_size) const {{" # For a single field, just inline it for simplicity if len(size_calc) == 1 and len(size_calc[0]) + len(o) + 3 < 120: @@ -1101,7 +1393,7 @@ def build_message_type( cpp += o prot = "void calculate_size(uint32_t &total_size) const override;" public_content.append(prot) - # If no fields to calculate size for, the default implementation in ProtoMessage will be used + # If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used # dump_to method declaration in header prot = "#ifdef HAS_PROTO_MESSAGE_DUMP\n" @@ -1132,7 +1424,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" @@ -1168,6 +1462,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): @@ -1198,8 +1503,10 @@ def find_common_fields( if not messages: return [] - # Start with fields from the first message - first_msg_fields = {field.name: field for field in messages[0].field} + # Start with fields from the first message (excluding deprecated fields) + first_msg_fields = { + field.name: field for field in messages[0].field if not field.options.deprecated + } common_fields = [] # Check each field to see if it exists in all messages with same type @@ -1210,6 +1517,9 @@ def find_common_fields( for msg in messages[1:]: found = False for other_field in msg.field: + # Skip deprecated fields + if other_field.options.deprecated: + continue if ( other_field.name == field_name and other_field.type == field.type @@ -1230,9 +1540,33 @@ def find_common_fields( return common_fields +def get_common_field_ifdef( + field_name: str, messages: list[descriptor.DescriptorProto] +) -> str | None: + """Get the field_ifdef option if it's consistent across all messages. + + Args: + field_name: Name of the field to check + messages: List of messages that contain this field + + Returns: + The field_ifdef string if all messages have the same value, None otherwise + """ + field_ifdefs = { + get_field_opt(field, pb.field_ifdef) + for msg in messages + if (field := next((f for f in msg.field if f.name == field_name), None)) + } + + # Return the ifdef only if all messages agree on the same value + return field_ifdefs.pop() if len(field_ifdefs) == 1 else None + + 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 = [] @@ -1241,17 +1575,26 @@ 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) + + # Get field_ifdef if it's consistent across all messages + field_ifdef = get_common_field_ifdef(field.name, messages) # Only add field declarations, not encode/decode logic - protected_content.extend(ti.protected_content) - public_content.extend(ti.public_content) + 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)) + + # 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 @@ -1277,6 +1620,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 = [] @@ -1289,7 +1633,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) @@ -1299,14 +1645,19 @@ 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.""" + # Skip deprecated messages + if mt.options.deprecated: + return None + snake = camel_to_snake(mt.name) id_: int | None = get_opt(mt, pb.id) 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) @@ -1366,7 +1717,6 @@ def main() -> None: #include "esphome/core/defines.h" #include "proto.h" -#include "api_pb2_size.h" namespace esphome { namespace api { @@ -1376,9 +1726,9 @@ namespace api { cpp = FILE_HEADER cpp += """\ #include "api_pb2.h" - #include "api_pb2_size.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" + #include namespace esphome { namespace api { @@ -1403,12 +1753,18 @@ 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, used_messages = ( + build_type_usage_map(file) + ) # Simple grouping of enums by ifdef current_ifdef = None for enum in file.enum_type: + # Skip deprecated enums + if enum.options.deprecated: + continue + s, c, dc = build_enum_type(enum, enum_ifdef_map) enum_ifdef = enum_ifdef_map.get(enum.name) @@ -1447,7 +1803,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 @@ -1457,7 +1815,15 @@ namespace api { current_ifdef = None for m in mt: - s, c, dc = build_message_type(m, base_class_fields) + # Skip deprecated messages + if m.options.deprecated: + continue + + # Skip messages that aren't used (unless they have an ID/service message) + if m.name not in used_messages and not m.options.HasExtension(pb.id): + continue + + s, c, dc = build_message_type(m, base_class_fields, message_source_map) msg_ifdef = message_ifdef_map.get(m.name) # Handle ifdef changes @@ -1549,13 +1915,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 @@ -1566,7 +1931,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 @@ -1641,7 +2006,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" @@ -1654,7 +2021,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" @@ -1701,7 +2068,6 @@ static const char *const TAG = "api.service"; exec_clang_format(root / "api_pb2_service.cpp") exec_clang_format(root / "api_pb2.h") exec_clang_format(root / "api_pb2.cpp") - exec_clang_format(root / "api_pb2_dump.h") exec_clang_format(root / "api_pb2_dump.cpp") except ImportError: pass diff --git a/script/build_codeowners.py b/script/build_codeowners.py index 523fe8ac7f..4581620095 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -31,6 +31,7 @@ BASE = """ pyproject.toml @esphome/core esphome/*.py @esphome/core esphome/core/* @esphome/core +.github/** @esphome/core # Integrations """.strip() diff --git a/script/ci-custom.py b/script/ci-custom.py index d0b518251f..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): @@ -270,7 +273,7 @@ def lint_newline(fname): return "File contains Windows newline. Please set your editor to Unix newline mode." -@lint_content_check(exclude=["*.svg"]) +@lint_content_check(exclude=["*.svg", ".clang-tidy.hash"]) def lint_end_newline(fname, content): if content and not content.endswith("\n"): return "File does not end with a newline, please add an empty line at the end of the file." diff --git a/script/clang-tidy b/script/clang-tidy index 5baaaf6b3a..b5905e0e4e 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -22,6 +22,7 @@ from helpers import ( git_ls_files, load_idedata, print_error_for_file, + print_file_list, root_path, temp_header_file, ) @@ -218,13 +219,14 @@ def main(): ) args = parser.parse_args() - idedata = load_idedata(args.environment) - options = clang_options(idedata) - files = [] for path in git_ls_files(["*.cpp"]): files.append(os.path.relpath(path, os.getcwd())) + # Print initial file count if it's large + if len(files) > 50: + print(f"Found {len(files)} total files to process") + if args.files: # Match against files specified on command-line file_name_re = re.compile("|".join(args.files)) @@ -240,10 +242,28 @@ def main(): if args.split_num: files = split_list(files, args.split_num)[args.split_at - 1] + print(f"Split {args.split_at}/{args.split_num}: checking {len(files)} files") + # Print file count before adding header file + print(f"\nTotal files to check: {len(files)}") + + # Early exit if no files to check + if not files: + print("No files to check - exiting early") + return 0 + + # Only build header file if we have actual files to check if args.all_headers and args.split_at in (None, 1): build_all_include() files.insert(0, temp_header_file) + print(f"Added all-include header file, new total: {len(files)}") + + # Print final file list before loading idedata + print_file_list(files, "Final files to process:") + + # Load idedata and options only if we have files to check + idedata = load_idedata(args.environment) + options = clang_options(idedata) tmpdir = None if args.fix: diff --git a/script/clang_tidy_hash.py b/script/clang_tidy_hash.py new file mode 100755 index 0000000000..19eb2a825e --- /dev/null +++ b/script/clang_tidy_hash.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Calculate and manage hash for clang-tidy configuration.""" + +from __future__ import annotations + +import argparse +import hashlib +from pathlib import Path +import re +import sys + +# Add the script directory to path to import helpers +script_dir = Path(__file__).parent +sys.path.insert(0, str(script_dir)) + + +def read_file_lines(path: Path) -> list[str]: + """Read lines from a file.""" + with open(path) as f: + return f.readlines() + + +def parse_requirement_line(line: str) -> tuple[str, str] | None: + """Parse a requirement line and return (package, original_line) or None. + + Handles formats like: + - package==1.2.3 + - package==1.2.3 # comment + - package>=1.2.3,<2.0.0 + """ + original_line = line.strip() + + # Extract the part before any comment for parsing + parse_line = line + if "#" in parse_line: + parse_line = parse_line[: parse_line.index("#")] + + parse_line = parse_line.strip() + if not parse_line: + return None + + # Use regex to extract package name + # This matches package names followed by version operators + match = re.match(r"^([a-zA-Z0-9_-]+)(==|>=|<=|>|<|!=|~=)(.+)$", parse_line) + if match: + return (match.group(1), original_line) # Return package name and original line + + return None + + +def get_clang_tidy_version_from_requirements() -> str: + """Get clang-tidy version from requirements_dev.txt""" + requirements_path = Path(__file__).parent.parent / "requirements_dev.txt" + lines = read_file_lines(requirements_path) + + for line in lines: + parsed = parse_requirement_line(line) + if parsed and parsed[0] == "clang-tidy": + # Return the original line (preserves comments) + return parsed[1] + + return "clang-tidy version not found" + + +def read_file_bytes(path: Path) -> bytes: + """Read bytes from a file.""" + with open(path, "rb") as f: + return f.read() + + +def calculate_clang_tidy_hash() -> str: + """Calculate hash of clang-tidy configuration and version""" + hasher = hashlib.sha256() + + # Hash .clang-tidy file + clang_tidy_path = Path(__file__).parent.parent / ".clang-tidy" + content = read_file_bytes(clang_tidy_path) + hasher.update(content) + + # Hash clang-tidy version from requirements_dev.txt + version = get_clang_tidy_version_from_requirements() + hasher.update(version.encode()) + + # Hash the entire platformio.ini file + platformio_path = Path(__file__).parent.parent / "platformio.ini" + platformio_content = read_file_bytes(platformio_path) + hasher.update(platformio_content) + + return hasher.hexdigest() + + +def read_stored_hash() -> str | None: + """Read the stored hash from file""" + hash_file = Path(__file__).parent.parent / ".clang-tidy.hash" + if hash_file.exists(): + lines = read_file_lines(hash_file) + return lines[0].strip() if lines else None + return None + + +def write_file_content(path: Path, content: str) -> None: + """Write content to a file.""" + with open(path, "w") as f: + f.write(content) + + +def write_hash(hash_value: str) -> None: + """Write hash to file""" + hash_file = Path(__file__).parent.parent / ".clang-tidy.hash" + # Strip any trailing newlines to ensure consistent formatting + write_file_content(hash_file, hash_value.strip() + "\n") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Manage clang-tidy configuration hash") + parser.add_argument( + "--check", + action="store_true", + help="Check if full scan needed (exit 0 if needed)", + ) + parser.add_argument("--update", action="store_true", help="Update the hash file") + parser.add_argument( + "--update-if-changed", + action="store_true", + help="Update hash only if configuration changed (for pre-commit)", + ) + parser.add_argument( + "--verify", action="store_true", help="Verify hash matches (for CI)" + ) + + args = parser.parse_args() + + current_hash = calculate_clang_tidy_hash() + stored_hash = read_stored_hash() + + if args.check: + # Exit 0 if full scan needed (hash changed or no hash file) + sys.exit(0 if current_hash != stored_hash else 1) + + elif args.update: + write_hash(current_hash) + print(f"Hash updated: {current_hash}") + + elif args.update_if_changed: + if current_hash != stored_hash: + write_hash(current_hash) + print(f"Clang-tidy hash updated: {current_hash}") + # Exit 0 so pre-commit can stage the file + sys.exit(0) + else: + print("Clang-tidy hash unchanged") + sys.exit(0) + + elif args.verify: + if current_hash != stored_hash: + print("ERROR: Clang-tidy configuration has changed but hash not updated!") + print(f"Expected: {current_hash}") + print(f"Found: {stored_hash}") + print("\nPlease run: script/clang_tidy_hash.py --update") + sys.exit(1) + print("Hash verification passed") + + else: + print(f"Current hash: {current_hash}") + print(f"Stored hash: {stored_hash}") + print(f"Match: {current_hash == stored_hash}") + + +if __name__ == "__main__": + main() diff --git a/script/determine-jobs.py b/script/determine-jobs.py new file mode 100755 index 0000000000..e26bc29c2f --- /dev/null +++ b/script/determine-jobs.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Determine which CI jobs should run based on changed files. + +This script is a centralized way to determine which CI jobs need to run based on +what files have changed. It outputs JSON with the following structure: + +{ + "integration_tests": true/false, + "clang_tidy": true/false, + "clang_format": true/false, + "python_linters": true/false, + "changed_components": ["component1", "component2", ...], + "component_test_count": 5 +} + +The CI workflow uses this information to: +- Skip or run integration tests +- Skip or run clang-tidy (and whether to do a full scan) +- Skip or run clang-format +- Skip or run Python linters (ruff, flake8, pylint, pyupgrade) +- Determine which components to test individually +- Decide how to split component tests (if there are many) + +Usage: + python script/determine-jobs.py [-b BRANCH] + +Options: + -b, --branch BRANCH Branch to compare against (default: dev) +""" + +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +import subprocess +import sys +from typing import Any + +from helpers import ( + CPP_FILE_EXTENSIONS, + ESPHOME_COMPONENTS_PATH, + PYTHON_FILE_EXTENSIONS, + changed_files, + get_all_dependencies, + get_components_from_integration_fixtures, + parse_list_components_output, + root_path, +) + + +def should_run_integration_tests(branch: str | None = None) -> bool: + """Determine if integration tests should run based on changed files. + + This function is used by the CI workflow to intelligently skip integration tests when they're + not needed, saving significant CI time and resources. + + Integration tests will run when ANY of the following conditions are met: + + 1. Core C++ files changed (esphome/core/*) + - Any .cpp, .h, .tcc files in the core directory + - These files contain fundamental functionality used throughout ESPHome + - Examples: esphome/core/component.cpp, esphome/core/application.h + + 2. Core Python files changed (esphome/core/*.py) + - Only .py files in the esphome/core/ directory + - These are core Python files that affect the entire system + - Examples: esphome/core/config.py, esphome/core/__init__.py + - NOT included: esphome/*.py, esphome/dashboard/*.py, esphome/components/*/*.py + + 3. Integration test files changed + - Any file in tests/integration/ directory + - This includes test files themselves and fixture YAML files + - Examples: tests/integration/test_api.py, tests/integration/fixtures/api.yaml + + 4. Components used by integration tests (or their dependencies) changed + - The function parses all YAML files in tests/integration/fixtures/ + - Extracts which components are used in integration tests + - Recursively finds all dependencies of those components + - If any of these components have changes, tests must run + - Example: If api.yaml uses 'sensor' and 'api' components, and 'api' depends on 'socket', + then changes to sensor/, api/, or socket/ components trigger tests + + Args: + branch: Branch to compare against. If None, uses default. + + Returns: + True if integration tests should run, False otherwise. + """ + files = changed_files(branch) + + # Check if any core files changed (esphome/core/*) + for file in files: + if file.startswith("esphome/core/"): + return True + + # Check if any integration test files changed + if any("tests/integration" in file for file in files): + return True + + # Get all components used in integration tests and their dependencies + fixture_components = get_components_from_integration_fixtures() + all_required_components = get_all_dependencies(fixture_components) + + # Check if any required components changed + for file in files: + if file.startswith(ESPHOME_COMPONENTS_PATH): + parts = file.split("/") + if len(parts) >= 3: + component = parts[2] + if component in all_required_components: + return True + + return False + + +def should_run_clang_tidy(branch: str | None = None) -> bool: + """Determine if clang-tidy should run based on changed files. + + This function is used by the CI workflow to intelligently skip clang-tidy checks when they're + not needed, saving significant CI time and resources. + + Clang-tidy will run when ANY of the following conditions are met: + + 1. Clang-tidy configuration changed + - The hash of .clang-tidy configuration file has changed + - The hash includes the .clang-tidy file, clang-tidy version from requirements_dev.txt, + and relevant platformio.ini sections + - When configuration changes, a full scan is needed to ensure all code complies + with the new rules + - Detected by script/clang_tidy_hash.py --check returning exit code 0 + + 2. Any C++ source files changed + - Any file with C++ extensions: .cpp, .h, .hpp, .cc, .cxx, .c, .tcc + - Includes files anywhere in the repository, not just in esphome/ + - This ensures all C++ code is checked, including tests, examples, etc. + - Examples: esphome/core/component.cpp, tests/custom/my_component.h + + 3. The .clang-tidy.hash file itself changed + - This indicates the configuration has been updated and clang-tidy should run + - Ensures that PRs updating the clang-tidy configuration are properly validated + + If the hash check fails for any reason, clang-tidy runs as a safety measure to ensure + code quality is maintained. + + Args: + branch: Branch to compare against. If None, uses default. + + Returns: + True if clang-tidy should run, False otherwise. + """ + # First check if clang-tidy configuration changed (full scan needed) + try: + result = subprocess.run( + [os.path.join(root_path, "script", "clang_tidy_hash.py"), "--check"], + capture_output=True, + check=False, + ) + # Exit 0 means hash changed (full scan needed) + if result.returncode == 0: + return True + except Exception: + # If hash check fails, run clang-tidy to be safe + return True + + # Check if .clang-tidy.hash file itself was changed + # This handles the case where the hash was properly updated in the PR + files = changed_files(branch) + if ".clang-tidy.hash" in files: + return True + + return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS) + + +def should_run_clang_format(branch: str | None = None) -> bool: + """Determine if clang-format should run based on changed files. + + This function is used by the CI workflow to skip clang-format checks when no C++ files + have changed, saving CI time and resources. + + Clang-format will run when any C++ source files have changed. + + Args: + branch: Branch to compare against. If None, uses default. + + Returns: + True if clang-format should run, False otherwise. + """ + return _any_changed_file_endswith(branch, CPP_FILE_EXTENSIONS) + + +def should_run_python_linters(branch: str | None = None) -> bool: + """Determine if Python linters (ruff, flake8, pylint, pyupgrade) should run based on changed files. + + This function is used by the CI workflow to skip Python linting checks when no Python files + have changed, saving CI time and resources. + + Python linters will run when any Python source files have changed. + + Args: + branch: Branch to compare against. If None, uses default. + + Returns: + True if Python linters should run, False otherwise. + """ + return _any_changed_file_endswith(branch, PYTHON_FILE_EXTENSIONS) + + +def _any_changed_file_endswith(branch: str | None, extensions: tuple[str, ...]) -> bool: + """Check if a changed file ends with any of the specified extensions.""" + return any(file.endswith(extensions) for file in changed_files(branch)) + + +def main() -> None: + """Main function that determines which CI jobs to run.""" + parser = argparse.ArgumentParser( + description="Determine which CI jobs should run based on changed files" + ) + parser.add_argument( + "-b", "--branch", help="Branch to compare changed files against" + ) + args = parser.parse_args() + + # Determine what should run + run_integration = should_run_integration_tests(args.branch) + run_clang_tidy = should_run_clang_tidy(args.branch) + run_clang_format = should_run_clang_format(args.branch) + run_python_linters = should_run_python_linters(args.branch) + + # Get changed components using list-components.py for exact compatibility + script_path = Path(__file__).parent / "list-components.py" + cmd = [sys.executable, str(script_path), "--changed"] + if args.branch: + cmd.extend(["-b", args.branch]) + + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + changed_components = parse_list_components_output(result.stdout) + + # Build output + output: dict[str, Any] = { + "integration_tests": run_integration, + "clang_tidy": run_clang_tidy, + "clang_format": run_clang_format, + "python_linters": run_python_linters, + "changed_components": changed_components, + "component_test_count": len(changed_components), + } + + # Output as JSON + print(json.dumps(output)) + + +if __name__ == "__main__": + main() diff --git a/script/helpers.py b/script/helpers.py index 1a0349e434..9032451b4f 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1,8 +1,14 @@ +from __future__ import annotations + +from functools import cache import json +import os import os.path from pathlib import Path import re import subprocess +import time +from typing import Any import colorama @@ -11,14 +17,42 @@ basepath = os.path.join(root_path, "esphome") temp_folder = os.path.join(root_path, ".temp") temp_header_file = os.path.join(temp_folder, "all-include.cpp") +# C++ file extensions used for clang-tidy and clang-format checks +CPP_FILE_EXTENSIONS = (".cpp", ".h", ".hpp", ".cc", ".cxx", ".c", ".tcc") -def styled(color, msg, reset=True): +# Python file extensions +PYTHON_FILE_EXTENSIONS = (".py", ".pyi") + +# YAML file extensions +YAML_FILE_EXTENSIONS = (".yaml", ".yml") + +# Component path prefix +ESPHOME_COMPONENTS_PATH = "esphome/components/" + + +def parse_list_components_output(output: str) -> list[str]: + """Parse the output from list-components.py script. + + The script outputs one component name per line. + + Args: + output: The stdout from list-components.py + + Returns: + List of component names, or empty list if no output + """ + if not output or not output.strip(): + return [] + return [c.strip() for c in output.strip().split("\n") if c.strip()] + + +def styled(color: str | tuple[str, ...], msg: str, reset: bool = True) -> str: prefix = "".join(color) if isinstance(color, tuple) else color suffix = colorama.Style.RESET_ALL if reset else "" return prefix + msg + suffix -def print_error_for_file(file, body): +def print_error_for_file(file: str, body: str | None) -> None: print( styled(colorama.Fore.GREEN, "### File ") + styled((colorama.Fore.GREEN, colorama.Style.BRIGHT), file) @@ -29,17 +63,22 @@ def print_error_for_file(file, body): print() -def build_all_include(): +def build_all_include() -> None: # Build a cpp file that includes all header files in this repo. # Otherwise header-only integrations would not be tested by clang-tidy - headers = [] - for path in walk_files(basepath): - filetypes = (".h",) - ext = os.path.splitext(path)[1] - if ext in filetypes: - path = os.path.relpath(path, root_path) - include_p = path.replace(os.path.sep, "/") - headers.append(f'#include "{include_p}"') + + # Use git ls-files to find all .h files in the esphome directory + # This is much faster than walking the filesystem + cmd = ["git", "ls-files", "esphome/**/*.h"] + proc = subprocess.run(cmd, capture_output=True, text=True, check=True) + + # Process git output - git already returns paths relative to repo root + headers = [ + f'#include "{include_p}"' + for line in proc.stdout.strip().split("\n") + if (include_p := line.replace(os.path.sep, "/")) + ] + headers.sort() headers.append("") content = "\n".join(headers) @@ -48,29 +87,87 @@ def build_all_include(): p.write_text(content, encoding="utf-8") -def walk_files(path): - for root, _, files in os.walk(path): - for name in files: - yield os.path.join(root, name) - - -def get_output(*args): +def get_output(*args: str) -> str: with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: output, _ = proc.communicate() return output.decode("utf-8") -def get_err(*args): +def get_err(*args: str) -> str: with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: _, err = proc.communicate() return err.decode("utf-8") -def splitlines_no_ends(string): +def splitlines_no_ends(string: str) -> list[str]: return [s.strip() for s in string.splitlines()] -def changed_files(branch="dev"): +def _get_pr_number_from_github_env() -> str | None: + """Extract PR number from GitHub environment variables. + + Returns: + PR number as string, or None if not found + """ + # First try parsing GITHUB_REF (fastest) + github_ref = os.environ.get("GITHUB_REF", "") + if "/pull/" in github_ref: + return github_ref.split("/pull/")[1].split("/")[0] + + # Fallback to GitHub event file + github_event_path = os.environ.get("GITHUB_EVENT_PATH") + if github_event_path and os.path.exists(github_event_path): + with open(github_event_path) as f: + event_data = json.load(f) + pr_data = event_data.get("pull_request", {}) + if pr_number := pr_data.get("number"): + return str(pr_number) + + return None + + +@cache +def _get_changed_files_github_actions() -> list[str] | None: + """Get changed files in GitHub Actions environment. + + Returns: + List of changed files, or None if should fall back to git method + """ + event_name = os.environ.get("GITHUB_EVENT_NAME") + + # For pull requests + if event_name == "pull_request": + pr_number = _get_pr_number_from_github_env() + if pr_number: + # Use GitHub CLI to get changed files directly + cmd = ["gh", "pr", "diff", pr_number, "--name-only"] + return _get_changed_files_from_command(cmd) + + # For pushes (including squash-and-merge) + elif event_name == "push": + # For push events, we want to check what changed in this commit + try: + # Get the changed files in the last commit + return _get_changed_files_from_command( + ["git", "diff", "HEAD~1..HEAD", "--name-only"] + ) + except: # noqa: E722 + # Fall back to the original method if this fails + pass + + return None + + +def changed_files(branch: str | None = None) -> list[str]: + # In GitHub Actions, we can use the API to get changed files more efficiently + if os.environ.get("GITHUB_ACTIONS") == "true": + github_files = _get_changed_files_github_actions() + if github_files is not None: + return github_files + + # Original implementation for local development + if not branch: # Treat None and empty string the same + branch = "dev" check_remotes = ["upstream", "origin"] check_remotes.extend(splitlines_no_ends(get_output("git", "remote"))) for remote in check_remotes: @@ -83,25 +180,165 @@ def changed_files(branch="dev"): pass else: raise ValueError("Git not configured") - command = ["git", "diff", merge_base, "--name-only"] - changed = splitlines_no_ends(get_output(*command)) - changed = [os.path.relpath(f, os.getcwd()) for f in changed] - changed.sort() - return changed + return _get_changed_files_from_command(["git", "diff", merge_base, "--name-only"]) -def filter_changed(files): +def _get_changed_files_from_command(command: list[str]) -> list[str]: + """Run a git command to get changed files and return them as a list.""" + proc = subprocess.run(command, capture_output=True, text=True, check=False) + if proc.returncode != 0: + raise Exception(f"Command failed: {' '.join(command)}\nstderr: {proc.stderr}") + + changed_files = splitlines_no_ends(proc.stdout) + changed_files = [os.path.relpath(f, os.getcwd()) for f in changed_files if f] + changed_files.sort() + return changed_files + + +def get_changed_components() -> list[str] | None: + """Get list of changed components using list-components.py script. + + This function: + 1. First checks if any core C++/header files (esphome/core/*.{cpp,h,hpp,cc,cxx,c}) changed - if so, returns None + 2. Otherwise delegates to ./script/list-components.py --changed which: + - Analyzes all changed files + - Determines which components are affected (including dependencies) + - Returns a list of component names that need to be checked + + Returns: + - None: Core C++/header files changed, need full scan + - Empty list: No components changed (only non-component files changed) + - List of strings: Names of components that need checking (e.g., ["wifi", "mqtt"]) + """ + # Check if any core C++ or header files changed first changed = changed_files() - files = [f for f in files if f in changed] - print("Changed files:") - if not files: - print(" No changed files!") - for c in files: - print(f" {c}") + core_cpp_changed = any( + f.startswith("esphome/core/") + and f.endswith(CPP_FILE_EXTENSIONS[:-1]) # Exclude .tcc for core files + for f in changed + ) + if core_cpp_changed: + print("Core C++/header files changed - will run full clang-tidy scan") + return None + + # Use list-components.py to get changed components + script_path = os.path.join(root_path, "script", "list-components.py") + cmd = [script_path, "--changed"] + + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, close_fds=False + ) + return parse_list_components_output(result.stdout) + except subprocess.CalledProcessError: + # If the script fails, fall back to full scan + print("Could not determine changed components - will run full clang-tidy scan") + return None + + +def _filter_changed_ci(files: list[str]) -> list[str]: + """Filter files based on changed components in CI environment. + + This function implements intelligent filtering to reduce CI runtime by only + checking files that could be affected by the changes. It handles three scenarios: + + 1. Core C++/header files changed (returns None from get_changed_components): + - Triggered when any C++/header file in esphome/core/ is modified + - Action: Check ALL files (full scan) + - Reason: Core C++/header files are used throughout the codebase + + 2. No components changed (returns empty list from get_changed_components): + - Triggered when only non-component files changed (e.g., scripts, configs) + - Action: Check only the specific non-component files that changed + - Example: If only script/clang-tidy changed, only check that file + + 3. Specific components changed (returns list of component names): + - Component detection done by: ./script/list-components.py --changed + - That script analyzes which components are affected by the changed files + INCLUDING their dependencies + - Action: Check ALL files in each component that list-components.py identifies + - Example: If wifi.cpp changed, list-components.py might return ["wifi", "network"] + if network depends on wifi. We then check ALL files in both + esphome/components/wifi/ and esphome/components/network/ + - Reason: Component files often have interdependencies (headers, base classes) + + Args: + files: List of all files that clang-tidy would normally check + + Returns: + Filtered list of files to check + """ + components = get_changed_components() + if components is None: + # Scenario 1: Core files changed or couldn't determine components + # Action: Return all files for full scan + return files + + if not components: + # Scenario 2: No components changed - only non-component files changed + # Action: Check only the specific non-component files that changed + changed = changed_files() + files = [ + f + for f in files + if f in changed and not f.startswith(ESPHOME_COMPONENTS_PATH) + ] + if not files: + print("No files changed") + return files + + # Scenario 3: Specific components changed + # Action: Check ALL files in each changed component + # Convert component list to set for O(1) lookups + component_set = set(components) + print(f"Changed components: {', '.join(sorted(components))}") + + # The 'files' parameter contains ALL files in the codebase that clang-tidy would check. + # We filter this down to only files in the changed components. + # We check ALL files in each changed component (not just the changed files) + # because changes in one file can affect other files in the same component. + filtered_files = [] + for f in files: + if f.startswith(ESPHOME_COMPONENTS_PATH): + # Check if file belongs to any of the changed components + parts = f.split("/") + if len(parts) >= 3 and parts[2] in component_set: + filtered_files.append(f) + + return filtered_files + + +def _filter_changed_local(files: list[str]) -> list[str]: + """Filter files based on git changes for local development. + + Args: + files: List of all files to filter + + Returns: + Filtered list of files to check + """ + # For local development, just check changed files directly + changed = changed_files() + return [f for f in files if f in changed] + + +def filter_changed(files: list[str]) -> list[str]: + """Filter files to only those that changed or are in changed components. + + Args: + files: List of files to filter + """ + # When running from CI, use component-based filtering + if os.environ.get("GITHUB_ACTIONS") == "true": + files = _filter_changed_ci(files) + else: + files = _filter_changed_local(files) + + print_file_list(files, "Files to check after filtering:") return files -def filter_grep(files, value): +def filter_grep(files: list[str], value: str) -> list[str]: matched = [] for file in files: with open(file, encoding="utf-8") as handle: @@ -111,7 +348,7 @@ def filter_grep(files, value): return matched -def git_ls_files(patterns=None): +def git_ls_files(patterns: list[str] | None = None) -> dict[str, int]: command = ["git", "ls-files", "-s"] if patterns is not None: command.extend(patterns) @@ -121,7 +358,10 @@ def git_ls_files(patterns=None): return {s[3].strip(): int(s[0]) for s in lines} -def load_idedata(environment): +def load_idedata(environment: str) -> dict[str, Any]: + start_time = time.time() + print(f"Loading IDE data for environment '{environment}'...") + platformio_ini = Path(root_path) / "platformio.ini" temp_idedata = Path(temp_folder) / f"idedata-{environment}.json" changed = False @@ -142,7 +382,10 @@ def load_idedata(environment): changed = True if not changed: - return json.loads(temp_idedata.read_text()) + data = json.loads(temp_idedata.read_text()) + elapsed = time.time() - start_time + print(f"IDE data loaded from cache in {elapsed:.2f} seconds") + return data # ensure temp directory exists before running pio, as it writes sdkconfig to it Path(temp_folder).mkdir(exist_ok=True) @@ -158,6 +401,9 @@ def load_idedata(environment): match = re.search(r'{\s*".*}', stdout.decode("utf-8")) data = json.loads(match.group()) temp_idedata.write_text(json.dumps(data, indent=2) + "\n") + + elapsed = time.time() - start_time + print(f"IDE data generated and cached in {elapsed:.2f} seconds") return data @@ -196,6 +442,29 @@ def get_binary(name: str, version: str) -> str: raise +def print_file_list( + files: list[str], title: str = "Files:", max_files: int = 20 +) -> None: + """Print a list of files with optional truncation for large lists. + + Args: + files: List of file paths to print + title: Title to print before the list + max_files: Maximum number of files to show before truncating (default: 20) + """ + print(title) + if not files: + print(" No files to check!") + elif len(files) <= max_files: + for f in sorted(files): + print(f" {f}") + else: + sorted_files = sorted(files) + for f in sorted_files[:10]: + print(f" {f}") + print(f" ... and {len(files) - 10} more files") + + def get_usable_cpu_count() -> int: """Return the number of CPUs that can be used for processes. @@ -205,3 +474,82 @@ def get_usable_cpu_count() -> int: return ( os.process_cpu_count() if hasattr(os, "process_cpu_count") else os.cpu_count() ) + + +def get_all_dependencies(component_names: set[str]) -> set[str]: + """Get all dependencies for a set of components. + + Args: + component_names: Set of component names to get dependencies for + + Returns: + Set of all components including dependencies and auto-loaded components + """ + from esphome.const import KEY_CORE + from esphome.core import CORE + from esphome.loader import get_component + + all_components: set[str] = set(component_names) + + # Reset CORE to ensure clean state + CORE.reset() + + # Set up fake config path for component loading + root = Path(__file__).parent.parent + CORE.config_path = str(root) + CORE.data[KEY_CORE] = {} + + # Keep finding dependencies until no new ones are found + while True: + new_components: set[str] = set() + + for comp_name in all_components: + comp = get_component(comp_name) + if not comp: + continue + + # Add dependencies (extract component name before '.') + new_components.update(dep.split(".")[0] for dep in comp.dependencies) + + # Add auto_load components + new_components.update(comp.auto_load) + + # Check if we found any new components + new_components -= all_components + if not new_components: + break + + all_components.update(new_components) + + return all_components + + +def get_components_from_integration_fixtures() -> set[str]: + """Extract all components used in integration test fixtures. + + Returns: + Set of component names used in integration test fixtures + """ + 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"): + 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 + + 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/script/list-components.py b/script/list-components.py index 0afcaa0f9d..66212f44e7 100755 --- a/script/list-components.py +++ b/script/list-components.py @@ -20,6 +20,12 @@ def filter_component_files(str): return str.startswith("esphome/components/") | str.startswith("tests/components/") +def get_all_component_files() -> list[str]: + """Get all component files from git.""" + files = git_ls_files() + return list(filter(filter_component_files, files)) + + def extract_component_names_array_from_files_array(files): components = [] for file in files: @@ -165,17 +171,20 @@ def main(): if args.branch and not args.changed: parser.error("--branch requires --changed") - files = git_ls_files() - files = filter(filter_component_files, files) - if args.changed: - if args.branch: - changed = changed_files(args.branch) - else: - changed = changed_files() + # When --changed is passed, only get the changed files + changed = changed_files(args.branch) + # If any base test file(s) changed, there's no need to filter out components - if not any("tests/test_build_components" in file for file in changed): - files = [f for f in files if f in changed] + if any("tests/test_build_components" in file for file in changed): + # Need to get all component files + files = get_all_component_files() + else: + # Only look at changed component files + files = [f for f in changed if filter_component_files(f)] + else: + # Get all component files + files = get_all_component_files() for c in get_components(files, args.changed): print(c) diff --git a/script/test_build_components b/script/test_build_components index 83ab947fc1..3796280176 100755 --- a/script/test_build_components +++ b/script/test_build_components @@ -72,7 +72,7 @@ for f in ./tests/components/$target_component/*.*.yaml; do if [ "$target_platform" = "all" ] || [ $file_name_parts = 2 ]; then # Test has *not* defined a specific target platform. Need to run tests for all possible target platforms. - + for target_platform_file in ./tests/test_build_components/build_components_base.*.yaml; do IFS='/' read -r -a folder_name <<< "$target_platform_file" IFS='.' read -r -a file_name <<< "${folder_name[3]}" @@ -83,7 +83,7 @@ for f in ./tests/components/$target_component/*.*.yaml; do else # Test has defined a specific target platform. - + # Validate we have a base test yaml for selected platform. # The target_platform is sourced from the following location. # 1. `./tests/test_build_components/build_components_base.[target_platform].yaml` 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/gpio/test_gpio_binary_sensor.py b/tests/component_tests/gpio/test_gpio_binary_sensor.py new file mode 100644 index 0000000000..74fa2ab1c1 --- /dev/null +++ b/tests/component_tests/gpio/test_gpio_binary_sensor.py @@ -0,0 +1,69 @@ +"""Tests for the GPIO binary sensor component.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +import pytest + + +def test_gpio_binary_sensor_basic_setup( + generate_main: Callable[[str | Path], str], +) -> None: + """ + When the GPIO binary sensor is set in the yaml file, it should be registered in main + """ + main_cpp = generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml") + + assert "new gpio::GPIOBinarySensor();" in main_cpp + assert "App.register_binary_sensor" in main_cpp + assert "bs_gpio->set_use_interrupt(true);" in main_cpp + assert "bs_gpio->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp + + +def test_gpio_binary_sensor_esp8266_gpio16_disables_interrupt( + generate_main: Callable[[str | Path], str], + caplog: pytest.LogCaptureFixture, +) -> None: + """ + Test that ESP8266 GPIO16 automatically disables interrupt mode with a warning + """ + main_cpp = generate_main( + "tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml" + ) + + # Check that interrupt is disabled for GPIO16 + assert "bs_gpio16->set_use_interrupt(false);" in main_cpp + + # Check that the warning was logged + assert "GPIO16 on ESP8266 doesn't support interrupts" in caplog.text + assert "Falling back to polling mode" in caplog.text + + +def test_gpio_binary_sensor_esp8266_other_pins_use_interrupt( + generate_main: Callable[[str | Path], str], +) -> None: + """ + Test that ESP8266 pins other than GPIO16 still use interrupt mode + """ + main_cpp = generate_main( + "tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml" + ) + + # GPIO5 should still use interrupts + assert "bs_gpio5->set_use_interrupt(true);" in main_cpp + assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp + + +def test_gpio_binary_sensor_explicit_polling_mode( + generate_main: Callable[[str | Path], str], +) -> None: + """ + Test that explicitly setting use_interrupt: false works + """ + main_cpp = generate_main( + "tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml" + ) + + assert "bs_polling->set_use_interrupt(false);" in main_cpp diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor.yaml b/tests/component_tests/gpio/test_gpio_binary_sensor.yaml new file mode 100644 index 0000000000..e258fe0cb4 --- /dev/null +++ b/tests/component_tests/gpio/test_gpio_binary_sensor.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + +esp32: + board: esp32dev + +binary_sensor: + - platform: gpio + pin: 5 + name: "Test GPIO Binary Sensor" + id: bs_gpio diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml b/tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml new file mode 100644 index 0000000000..aec26fe572 --- /dev/null +++ b/tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml @@ -0,0 +1,20 @@ +esphome: + name: test + +esp8266: + board: d1_mini + +binary_sensor: + - platform: gpio + pin: + number: 16 + mode: INPUT_PULLDOWN_16 + name: "GPIO16 Touch Sensor" + id: bs_gpio16 + + - platform: gpio + pin: + number: 5 + mode: INPUT_PULLUP + name: "GPIO5 Button" + id: bs_gpio5 diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml b/tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml new file mode 100644 index 0000000000..7c53e09265 --- /dev/null +++ b/tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +esp32: + board: esp32dev + +binary_sensor: + - platform: gpio + pin: 5 + name: "Polling Mode Sensor" + id: bs_polling + use_interrupt: false 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/animation/.gitattributes b/tests/components/animation/.gitattributes index ff9fc6f1f1..766b86f19e 100644 --- a/tests/components/animation/.gitattributes +++ b/tests/components/animation/.gitattributes @@ -1,4 +1,3 @@ *.apng -text *.webp -text *.gif -text - diff --git a/tests/components/animation/test.esp32-ard.yaml b/tests/components/animation/test.esp32-ard.yaml index 5d330900df..7d9fe45bff 100644 --- a/tests/components/animation/test.esp32-ard.yaml +++ b/tests/components/animation/test.esp32-ard.yaml @@ -15,4 +15,3 @@ display: packages: animation: !include common.yaml - diff --git a/tests/components/ble_presence/common.yaml b/tests/components/ble_presence/common.yaml index 6e5173eed8..2ba6aa0754 100644 --- a/tests/components/ble_presence/common.yaml +++ b/tests/components/ble_presence/common.yaml @@ -21,4 +21,3 @@ binary_sensor: - platform: ble_presence irk: 1234567890abcdef1234567890abcdef name: "ESP32 BLE Tracker with Identity Resolving Key" - diff --git a/tests/components/captive_portal/test.bk72xx-ard.yaml b/tests/components/captive_portal/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/captive_portal/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/color/common.yaml b/tests/components/color/common.yaml index 88524e6a5f..69a77326f7 100644 --- a/tests/components/color/common.yaml +++ b/tests/components/color/common.yaml @@ -17,4 +17,3 @@ color: hex: 008000 - id: cps_blue hex: 000080 - diff --git a/tests/components/const/common.yaml b/tests/components/const/common.yaml index 655af304af..f4b15f2b90 100644 --- a/tests/components/const/common.yaml +++ b/tests/components/const/common.yaml @@ -41,4 +41,3 @@ display: - delay 120ms - [0x29] - delay 20ms - diff --git a/tests/components/cst816/common.yaml b/tests/components/cst816/common.yaml index 9750de15db..9400e4eef0 100644 --- a/tests/components/cst816/common.yaml +++ b/tests/components/cst816/common.yaml @@ -42,4 +42,3 @@ binary_sensor: x_max: 480 y_min: 320 y_max: 360 - diff --git a/tests/components/debug/common.yaml b/tests/components/debug/common.yaml index a9d74e6865..d9a61f8df0 100644 --- a/tests/components/debug/common.yaml +++ b/tests/components/debug/common.yaml @@ -15,4 +15,3 @@ sensor: name: "Loop Time" cpu_frequency: name: "CPU Frequency" - diff --git a/tests/components/ens160_spi/common.yaml b/tests/components/ens160_spi/common.yaml index 7250ead228..c8b663272f 100644 --- a/tests/components/ens160_spi/common.yaml +++ b/tests/components/ens160_spi/common.yaml @@ -14,4 +14,3 @@ sensor: name: "ENS160 Total Volatile Organic Compounds" aqi: name: "ENS160 Air Quality Index" - diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index 582b2c22ce..ccf0b7cbd5 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -9,4 +9,3 @@ esp32: wifi: ssid: MySSID password: password1 - 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/esp_ldo/test.esp32-p4-idf.yaml b/tests/components/esp_ldo/test.esp32-p4-idf.yaml index 0e91aaf082..38bd6ac411 100644 --- a/tests/components/esp_ldo/test.esp32-p4-idf.yaml +++ b/tests/components/esp_ldo/test.esp32-p4-idf.yaml @@ -6,6 +6,7 @@ esp_ldo: - id: ldo_4 channel: 4 voltage: 2.0V + setup_priority: 900 esphome: on_boot: diff --git a/tests/components/esphome/test.esp32-ard.yaml b/tests/components/esphome/test.esp32-ard.yaml index a991f7e15a..dade44d145 100644 --- a/tests/components/esphome/test.esp32-ard.yaml +++ b/tests/components/esphome/test.esp32-ard.yaml @@ -1,2 +1 @@ <<: !include common.yaml - diff --git a/tests/components/esphome/test.esp32-c3-ard.yaml b/tests/components/esphome/test.esp32-c3-ard.yaml index a991f7e15a..dade44d145 100644 --- a/tests/components/esphome/test.esp32-c3-ard.yaml +++ b/tests/components/esphome/test.esp32-c3-ard.yaml @@ -1,2 +1 @@ <<: !include common.yaml - diff --git a/tests/components/font/.gitattributes b/tests/components/font/.gitattributes index 18d9a389e8..63ab00e9f2 100644 --- a/tests/components/font/.gitattributes +++ b/tests/components/font/.gitattributes @@ -1,2 +1 @@ *.pcf -text - diff --git a/tests/components/gl_r01_i2c/common.yaml b/tests/components/gl_r01_i2c/common.yaml new file mode 100644 index 0000000000..fe0705bdc6 --- /dev/null +++ b/tests/components/gl_r01_i2c/common.yaml @@ -0,0 +1,12 @@ +i2c: + - id: i2c_gl_r01_i2c + scl: ${scl_pin} + sda: ${sda_pin} + +sensor: + - platform: gl_r01_i2c + id: tof + name: "ToF sensor" + i2c_id: i2c_gl_r01_i2c + address: 0x74 + update_interval: 15s diff --git a/tests/components/gl_r01_i2c/test.esp32-ard.yaml b/tests/components/gl_r01_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/gl_r01_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml b/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/gl_r01_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml b/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp32-idf.yaml b/tests/components/gl_r01_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/gl_r01_i2c/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.esp8266-ard.yaml b/tests/components/gl_r01_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/gl_r01_i2c/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/gl_r01_i2c/test.rp2040-ard.yaml b/tests/components/gl_r01_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/gl_r01_i2c/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml 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/lps22/common.yaml b/tests/components/lps22/common.yaml new file mode 100644 index 0000000000..e6de4752ba --- /dev/null +++ b/tests/components/lps22/common.yaml @@ -0,0 +1,8 @@ +sensor: + - platform: lps22 + address: 0x5d + update_interval: 10s + temperature: + name: "LPS22 Temperature" + pressure: + name: "LPS22 Pressure" diff --git a/tests/components/lps22/test.esp32-ard.yaml b/tests/components/lps22/test.esp32-ard.yaml new file mode 100644 index 0000000000..0da6a9577e --- /dev/null +++ b/tests/components/lps22/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_lps22 + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/lps22/test.esp32-c3-ard.yaml b/tests/components/lps22/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..6091393d31 --- /dev/null +++ b/tests/components/lps22/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_lps22 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/lps22/test.esp32-c3-idf.yaml b/tests/components/lps22/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..6091393d31 --- /dev/null +++ b/tests/components/lps22/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_lps22 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/lps22/test.esp32-idf.yaml b/tests/components/lps22/test.esp32-idf.yaml new file mode 100644 index 0000000000..0da6a9577e --- /dev/null +++ b/tests/components/lps22/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_lps22 + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/lps22/test.esp8266-ard.yaml b/tests/components/lps22/test.esp8266-ard.yaml new file mode 100644 index 0000000000..6091393d31 --- /dev/null +++ b/tests/components/lps22/test.esp8266-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_lps22 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/lps22/test.rp2040-ard.yaml b/tests/components/lps22/test.rp2040-ard.yaml new file mode 100644 index 0000000000..6091393d31 --- /dev/null +++ b/tests/components/lps22/test.rp2040-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_lps22 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/lvgl/.gitattributes b/tests/components/lvgl/.gitattributes index 75e7a44254..9d74867fcf 100644 --- a/tests/components/lvgl/.gitattributes +++ b/tests/components/lvgl/.gitattributes @@ -1,2 +1 @@ *.ttf -text - diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 2edc62b6a1..46341c266d 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -919,21 +919,27 @@ lvgl: text_color: 0xFFFFFF scales: - ticks: - width: 1 + width: !lambda return 1; count: 61 - length: 20 + length: 20% color: 0xFFFFFF range_from: 0 range_to: 60 angle_range: 360 - rotation: 270 + 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 color: 0xFF0000 - r_mod: -1 - width: 3 + r_mod: !lambda return -1; + width: !lambda return 3; - angle_range: 330 rotation: 300 diff --git a/tests/components/lvgl/test.esp32-ard.yaml b/tests/components/lvgl/test.esp32-ard.yaml index 5b09147de7..f85bedbde6 100644 --- a/tests/components/lvgl/test.esp32-ard.yaml +++ b/tests/components/lvgl/test.esp32-ard.yaml @@ -56,4 +56,3 @@ lvgl: packages: lvgl: !include lvgl-package.yaml xvgl: !include common.yaml - diff --git a/tests/components/max17043/test.esp32-ard.yaml b/tests/components/max17043/test.esp32-ard.yaml index c84e0a5c2e..c6615f51cd 100644 --- a/tests/components/max17043/test.esp32-ard.yaml +++ b/tests/components/max17043/test.esp32-ard.yaml @@ -3,4 +3,3 @@ substitutions: scl_pin: GPIO22 <<: !include common.yaml - diff --git a/tests/components/mipi_spi/common.yaml b/tests/components/mipi_spi/common.yaml index e4b1e2b30c..2c84489ec7 100644 --- a/tests/components/mipi_spi/common.yaml +++ b/tests/components/mipi_spi/common.yaml @@ -35,4 +35,3 @@ display: allow_other_uses: true - number: ${enable_pin} bus_mode: single - 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/nextion/test.rp2040-ard.yaml b/tests/components/nextion/test.rp2040-ard.yaml index 20347c6eff..44534b97a8 100644 --- a/tests/components/nextion/test.rp2040-ard.yaml +++ b/tests/components/nextion/test.rp2040-ard.yaml @@ -4,4 +4,3 @@ substitutions: packages: base: !include common.yaml - diff --git a/tests/components/online_image/test.esp32-idf.yaml b/tests/components/online_image/test.esp32-idf.yaml index 3f01009812..d4af284151 100644 --- a/tests/components/online_image/test.esp32-idf.yaml +++ b/tests/components/online_image/test.esp32-idf.yaml @@ -1,4 +1,3 @@ <<: !include common-esp32.yaml http_request: - diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index bbcf48efa5..da5339fb39 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -11,4 +11,3 @@ openthread: pskc: 0xc23a76e98f1a6483639b1ac1271e2e27 mesh_local_prefix: fd53:145f:ed22:ad81::/64 force_dataset: true - diff --git a/tests/components/packages/test.esp32-ard.yaml b/tests/components/packages/test.esp32-ard.yaml index 7d0ab2b905..9e4ceb09d6 100644 --- a/tests/components/packages/test.esp32-ard.yaml +++ b/tests/components/packages/test.esp32-ard.yaml @@ -9,4 +9,3 @@ packages: file: common.yaml ref: dev refresh: 1d - diff --git a/tests/components/packages/test.esp32-idf.yaml b/tests/components/packages/test.esp32-idf.yaml index 8c0a34bb1a..5bf40822dc 100644 --- a/tests/components/packages/test.esp32-idf.yaml +++ b/tests/components/packages/test.esp32-idf.yaml @@ -11,4 +11,3 @@ packages: file: common.yaml ref: dev refresh: 1d - diff --git a/tests/components/qspi_dbi/common.yaml b/tests/components/qspi_dbi/common.yaml index 655af304af..f4b15f2b90 100644 --- a/tests/components/qspi_dbi/common.yaml +++ b/tests/components/qspi_dbi/common.yaml @@ -41,4 +41,3 @@ display: - delay 120ms - [0x29] - delay 20ms - 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/spi/test.esp32-p4-idf.yaml b/tests/components/spi/test.esp32-p4-idf.yaml index 061e3dd44a..93626dc1d5 100644 --- a/tests/components/spi/test.esp32-p4-idf.yaml +++ b/tests/components/spi/test.esp32-p4-idf.yaml @@ -35,4 +35,3 @@ spi: interface: any clk_pin: 8 mosi_pin: 9 - diff --git a/tests/components/spi/test.esp32-s3-idf.yaml b/tests/components/spi/test.esp32-s3-idf.yaml index 061e3dd44a..93626dc1d5 100644 --- a/tests/components/spi/test.esp32-s3-idf.yaml +++ b/tests/components/spi/test.esp32-s3-idf.yaml @@ -35,4 +35,3 @@ spi: interface: any clk_pin: 8 mosi_pin: 9 - 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/udp/common.yaml b/tests/components/udp/common.yaml index 79da02a692..96224d0d1f 100644 --- a/tests/components/udp/common.yaml +++ b/tests/components/udp/common.yaml @@ -17,4 +17,3 @@ udp: id: my_udp data: !lambda |- return std::vector{1,3,4,5,6}; - 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/components/web_server/test_ota.esp32-idf.yaml b/tests/components/web_server/test_ota.esp32-idf.yaml index 37838b3d34..99873aa27b 100644 --- a/tests/components/web_server/test_ota.esp32-idf.yaml +++ b/tests/components/web_server/test_ota.esp32-idf.yaml @@ -27,4 +27,3 @@ logger: logs: web_server: VERBOSE web_server_idf: VERBOSE - diff --git a/tests/components/wk2132_i2c/test.esp32-ard.yaml b/tests/components/wk2132_i2c/test.esp32-ard.yaml index 94552c5a40..3b761d3fc1 100644 --- a/tests/components/wk2132_i2c/test.esp32-ard.yaml +++ b/tests/components/wk2132_i2c/test.esp32-ard.yaml @@ -3,4 +3,3 @@ substitutions: sda_pin: GPIO21 <<: !include common.yaml - diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index aead6a73af..6e2f398f49 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -5,12 +5,14 @@ from __future__ import annotations import asyncio from collections.abc import AsyncGenerator, Callable, Generator from contextlib import AbstractAsyncContextManager, asynccontextmanager +import fcntl import logging import os from pathlib import Path import platform import signal import socket +import subprocess import sys import tempfile from typing import TextIO @@ -50,6 +52,66 @@ if platform.system() == "Windows": import pty # not available on Windows +def _get_platformio_env(cache_dir: Path) -> dict[str, str]: + """Get environment variables for PlatformIO with shared cache.""" + env = os.environ.copy() + env["PLATFORMIO_CORE_DIR"] = str(cache_dir) + env["PLATFORMIO_CACHE_DIR"] = str(cache_dir / ".cache") + env["PLATFORMIO_LIBDEPS_DIR"] = str(cache_dir / "libdeps") + return env + + +@pytest.fixture(scope="session") +def shared_platformio_cache() -> Generator[Path]: + """Initialize a shared PlatformIO cache for all integration tests.""" + # Use a dedicated directory for integration tests to avoid conflicts + test_cache_dir = Path.home() / ".esphome-integration-tests" + cache_dir = test_cache_dir / "platformio" + + # Use a lock file in the home directory to ensure only one process initializes the cache + # This is needed when running with pytest-xdist + # The lock file must be in a directory that already exists to avoid race conditions + lock_file = Path.home() / ".esphome-integration-tests-init.lock" + + # Always acquire the lock to ensure cache is ready before proceeding + with open(lock_file, "w") as lock_fd: + fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX) + + # Check if cache needs initialization while holding the lock + if not cache_dir.exists() or not any(cache_dir.iterdir()): + # Create the test cache directory if it doesn't exist + test_cache_dir.mkdir(exist_ok=True) + + with tempfile.TemporaryDirectory() as tmpdir: + # Create a basic host config + init_dir = Path(tmpdir) + config_path = init_dir / "cache_init.yaml" + config_path.write_text("""esphome: + name: cache-init +host: +api: + encryption: + key: "IIevImVI42I0FGos5nLqFK91jrJehrgidI0ArwMLr8w=" +logger: +""") + + # Run compilation to populate the cache + # We must succeed here to avoid race conditions where multiple + # tests try to populate the same cache directory simultaneously + env = _get_platformio_env(cache_dir) + + subprocess.run( + ["esphome", "compile", str(config_path)], + check=True, + cwd=init_dir, + env=env, + ) + + # Lock is held until here, ensuring cache is fully populated before any test proceeds + + yield cache_dir + + @pytest.fixture(scope="module", autouse=True) def enable_aioesphomeapi_debug_logging(): """Enable debug logging for aioesphomeapi to help diagnose connection issues.""" @@ -161,22 +223,14 @@ async def write_yaml_config( @pytest_asyncio.fixture async def compile_esphome( integration_test_dir: Path, + shared_platformio_cache: Path, ) -> AsyncGenerator[CompileFunction]: """Compile an ESPHome configuration and return the binary path.""" async def _compile(config_path: Path) -> Path: - # Create a unique PlatformIO directory for this test to avoid race conditions - platformio_dir = integration_test_dir / ".platformio" - platformio_dir.mkdir(parents=True, exist_ok=True) - - # Create cache directory as well - platformio_cache_dir = platformio_dir / ".cache" - platformio_cache_dir.mkdir(parents=True, exist_ok=True) - - # Set up environment with isolated PlatformIO directories - env = os.environ.copy() - env["PLATFORMIO_CORE_DIR"] = str(platformio_dir) - env["PLATFORMIO_CACHE_DIR"] = str(platformio_cache_dir) + # Use the shared PlatformIO cache for faster compilation + # This avoids re-downloading dependencies for each test + env = _get_platformio_env(shared_platformio_cache) # Retry compilation up to 3 times if we get a segfault max_retries = 3 @@ -341,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 @@ -521,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_custom_services.yaml b/tests/integration/fixtures/api_custom_services.yaml new file mode 100644 index 0000000000..41efc95b85 --- /dev/null +++ b/tests/integration/fixtures/api_custom_services.yaml @@ -0,0 +1,24 @@ +esphome: + name: api-custom-services-test +host: + +# This is required for CustomAPIDevice to work +api: + custom_services: true + # Also test that YAML services still work + actions: + - action: test_yaml_service + then: + - logger.log: "YAML service called" + +logger: + level: DEBUG + +# External component that uses CustomAPIDevice +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [custom_api_device_component] + +custom_api_device_component: 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 4a327b73a1..12ab070e55 100644 --- a/tests/integration/fixtures/areas_and_devices.yaml +++ b/tests/integration/fixtures/areas_and_devices.yaml @@ -55,3 +55,44 @@ sensor: 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/device_id_in_state.yaml b/tests/integration/fixtures/device_id_in_state.yaml index f2e320a2e2..c8548617b8 100644 --- a/tests/integration/fixtures/device_id_in_state.yaml +++ b/tests/integration/fixtures/device_id_in_state.yaml @@ -82,4 +82,3 @@ output: write_action: - lambda: |- ESP_LOGD("test", "Light output: %d", state); - 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/external_components/custom_api_device_component/__init__.py b/tests/integration/fixtures/external_components/custom_api_device_component/__init__.py new file mode 100644 index 0000000000..127082601a --- /dev/null +++ b/tests/integration/fixtures/external_components/custom_api_device_component/__init__.py @@ -0,0 +1,19 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +custom_api_device_component_ns = cg.esphome_ns.namespace("custom_api_device_component") +CustomAPIDeviceComponent = custom_api_device_component_ns.class_( + "CustomAPIDeviceComponent", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CustomAPIDeviceComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp new file mode 100644 index 0000000000..c8581b3d2f --- /dev/null +++ b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp @@ -0,0 +1,53 @@ +#include "custom_api_device_component.h" +#include "esphome/core/log.h" + +#ifdef USE_API +namespace esphome { +namespace custom_api_device_component { + +static const char *const TAG = "custom_api"; + +void CustomAPIDeviceComponent::setup() { + // Register services using CustomAPIDevice + register_service(&CustomAPIDeviceComponent::on_test_service, "custom_test_service"); + + register_service(&CustomAPIDeviceComponent::on_service_with_args, "custom_service_with_args", + {"arg_string", "arg_int", "arg_bool", "arg_float"}); + + // Test array types + register_service(&CustomAPIDeviceComponent::on_service_with_arrays, "custom_service_with_arrays", + {"bool_array", "int_array", "float_array", "string_array"}); +} + +void CustomAPIDeviceComponent::on_test_service() { ESP_LOGI(TAG, "Custom test service called!"); } + +// NOLINTNEXTLINE(performance-unnecessary-value-param) +void CustomAPIDeviceComponent::on_service_with_args(std::string arg_string, int32_t arg_int, bool arg_bool, + float arg_float) { + ESP_LOGI(TAG, "Custom service called with: %s, %d, %d, %.2f", arg_string.c_str(), arg_int, arg_bool, arg_float); +} + +void CustomAPIDeviceComponent::on_service_with_arrays(std::vector bool_array, std::vector int_array, + std::vector float_array, + std::vector string_array) { + ESP_LOGI(TAG, "Array service called with %zu bools, %zu ints, %zu floats, %zu strings", bool_array.size(), + int_array.size(), float_array.size(), string_array.size()); + + // Log first element of each array if not empty + if (!bool_array.empty()) { + ESP_LOGI(TAG, "First bool: %s", bool_array[0] ? "true" : "false"); + } + if (!int_array.empty()) { + ESP_LOGI(TAG, "First int: %d", int_array[0]); + } + if (!float_array.empty()) { + ESP_LOGI(TAG, "First float: %.2f", float_array[0]); + } + if (!string_array.empty()) { + ESP_LOGI(TAG, "First string: %s", string_array[0].c_str()); + } +} + +} // namespace custom_api_device_component +} // namespace esphome +#endif // USE_API diff --git a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h new file mode 100644 index 0000000000..92960746d9 --- /dev/null +++ b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include "esphome/core/component.h" +#include "esphome/components/api/custom_api_device.h" + +#ifdef USE_API +namespace esphome { +namespace custom_api_device_component { + +using namespace api; + +class CustomAPIDeviceComponent : public Component, public CustomAPIDevice { + public: + void setup() override; + + void on_test_service(); + + // NOLINTNEXTLINE(performance-unnecessary-value-param) + void on_service_with_args(std::string arg_string, int32_t arg_int, bool arg_bool, float arg_float); + + void on_service_with_arrays(std::vector bool_array, std::vector int_array, + std::vector float_array, std::vector string_array); +}; + +} // namespace custom_api_device_component +} // namespace esphome +#endif // USE_API diff --git a/tests/integration/fixtures/external_components/loop_test_component/__init__.py b/tests/integration/fixtures/external_components/loop_test_component/__init__.py index b66d4598f4..3f3a40db09 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/__init__.py +++ b/tests/integration/fixtures/external_components/loop_test_component/__init__.py @@ -1,7 +1,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME +from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME, CONF_UPDATE_INTERVAL CODEOWNERS = ["@esphome/tests"] @@ -10,10 +10,15 @@ LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Compon LoopTestISRComponent = loop_test_component_ns.class_( "LoopTestISRComponent", cg.Component ) +LoopTestUpdateComponent = loop_test_component_ns.class_( + "LoopTestUpdateComponent", cg.PollingComponent +) CONF_DISABLE_AFTER = "disable_after" CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations" CONF_ISR_COMPONENTS = "isr_components" +CONF_UPDATE_COMPONENTS = "update_components" +CONF_DISABLE_LOOP_AFTER = "disable_loop_after" COMPONENT_CONFIG_SCHEMA = cv.Schema( { @@ -31,11 +36,23 @@ ISR_COMPONENT_CONFIG_SCHEMA = cv.Schema( } ) +UPDATE_COMPONENT_CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(LoopTestUpdateComponent), + cv.Required(CONF_NAME): cv.string, + cv.Optional(CONF_DISABLE_LOOP_AFTER, default=0): cv.int_, + cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval, + } +) + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(LoopTestComponent), cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA), cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA), + cv.Optional(CONF_UPDATE_COMPONENTS): cv.ensure_list( + UPDATE_COMPONENT_CONFIG_SCHEMA + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -94,3 +111,12 @@ async def to_code(config): var = cg.new_Pvariable(isr_config[CONF_ID]) await cg.register_component(var, isr_config) cg.add(var.set_name(isr_config[CONF_NAME])) + + # Create update test components + for update_config in config.get(CONF_UPDATE_COMPONENTS, []): + var = cg.new_Pvariable(update_config[CONF_ID]) + await cg.register_component(var, update_config) + + cg.add(var.set_name(update_config[CONF_NAME])) + cg.add(var.set_disable_loop_after(update_config[CONF_DISABLE_LOOP_AFTER])) + cg.add(var.set_update_interval(update_config[CONF_UPDATE_INTERVAL])) diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp index 470740c534..28a05d3d45 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.cpp @@ -39,5 +39,29 @@ void LoopTestComponent::service_disable() { this->disable_loop(); } +// LoopTestUpdateComponent implementation +void LoopTestUpdateComponent::setup() { + ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent setup called", this->name_.c_str()); +} + +void LoopTestUpdateComponent::loop() { + this->loop_count_++; + ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent loop count: %d", this->name_.c_str(), this->loop_count_); + + // Disable loop after specified count to test component.update when loop is disabled + if (this->disable_loop_after_ > 0 && this->loop_count_ == this->disable_loop_after_) { + ESP_LOGI(TAG, "[%s] Disabling loop after %d iterations", this->name_.c_str(), this->disable_loop_after_); + this->disable_loop(); + } +} + +void LoopTestUpdateComponent::update() { + this->update_count_++; + // Check if loop is disabled by testing component state + bool loop_disabled = this->component_state_ == COMPONENT_STATE_LOOP_DONE; + ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent update() called, count: %d, loop_disabled: %s", this->name_.c_str(), + this->update_count_, loop_disabled ? "YES" : "NO"); +} + } // namespace loop_test_component } // namespace esphome diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h index 5c43dd4b43..cdc04d491b 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h @@ -4,6 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/automation.h" +#include "esphome/core/helpers.h" namespace esphome { namespace loop_test_component { @@ -54,5 +55,29 @@ template class DisableAction : public Action { LoopTestComponent *parent_; }; +// Component with update() method to test component.update action +class LoopTestUpdateComponent : public PollingComponent { + public: + LoopTestUpdateComponent() : PollingComponent(1000) {} // Default 1s update interval + + void set_name(const std::string &name) { this->name_ = name; } + void set_disable_loop_after(int count) { this->disable_loop_after_ = count; } + + void setup() override; + void loop() override; + void update() override; + + int get_update_count() const { return this->update_count_; } + int get_loop_count() const { return this->loop_count_; } + + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + std::string name_; + int loop_count_{0}; + int update_count_{0}; + int disable_loop_after_{0}; +}; + } // namespace loop_test_component } // namespace esphome diff --git a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp index d377c1fe57..8c3f665f19 100644 --- a/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp +++ b/tests/integration/fixtures/external_components/scheduler_string_lifetime_component/string_lifetime_component.cpp @@ -23,19 +23,6 @@ void SchedulerStringLifetimeComponent::run_string_lifetime_test() { test_vector_reallocation(); test_string_move_semantics(); test_lambda_capture_lifetime(); - - // Schedule final check - this->set_timeout("final_check", 200, [this]() { - ESP_LOGI(TAG, "String lifetime tests complete"); - ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_); - ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_); - - if (this->tests_failed_ == 0) { - ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!"); - } else { - ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_); - } - }); } void SchedulerStringLifetimeComponent::run_test1() { @@ -69,7 +56,6 @@ void SchedulerStringLifetimeComponent::run_test5() { } void SchedulerStringLifetimeComponent::run_final_check() { - ESP_LOGI(TAG, "String lifetime tests complete"); ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_); ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_); @@ -78,6 +64,7 @@ void SchedulerStringLifetimeComponent::run_final_check() { } else { ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_); } + ESP_LOGI(TAG, "String lifetime tests complete"); } void SchedulerStringLifetimeComponent::test_temporary_string_lifetime() { 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/large_message_batching.yaml b/tests/integration/fixtures/large_message_batching.yaml index 1b2d817cd4..e491da0486 100644 --- a/tests/integration/fixtures/large_message_batching.yaml +++ b/tests/integration/fixtures/large_message_batching.yaml @@ -134,4 +134,3 @@ switch: name: "Test Switch" id: test_switch optimistic: true - diff --git a/tests/integration/fixtures/loop_disable_enable.yaml b/tests/integration/fixtures/loop_disable_enable.yaml index f19d7f60ca..f87fe9130b 100644 --- a/tests/integration/fixtures/loop_disable_enable.yaml +++ b/tests/integration/fixtures/loop_disable_enable.yaml @@ -40,6 +40,13 @@ loop_test_component: - id: isr_test name: "isr_test" + # Update test component to test component.update when loop is disabled + update_components: + - id: update_test_component + name: "update_test" + disable_loop_after: 3 # Disable loop after 3 iterations + update_interval: 0.1s # Fast update interval for testing + # Interval to re-enable the self_disable_10 component after some time interval: - interval: 0.5s @@ -51,3 +58,28 @@ interval: - logger.log: "Re-enabling self_disable_10 via service" - loop_test_component.enable: id: self_disable_10 + + # Test component.update on a component with disabled loop + - interval: 0.1s + then: + - lambda: |- + static bool manual_update_done = false; + if (!manual_update_done && + id(update_test_component).get_loop_count() == 3 && + id(update_test_component).get_update_count() >= 3) { + ESP_LOGI("main", "Manually calling component.update on update_test_component with disabled loop"); + manual_update_done = true; + } + - if: + condition: + lambda: |- + static bool manual_update_triggered = false; + if (!manual_update_triggered && + id(update_test_component).get_loop_count() == 3 && + id(update_test_component).get_update_count() >= 3) { + manual_update_triggered = true; + return true; + } + return false; + then: + - component.update: update_test_component 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_null_name.yaml b/tests/integration/fixtures/scheduler_null_name.yaml new file mode 100644 index 0000000000..42eaacdd43 --- /dev/null +++ b/tests/integration/fixtures/scheduler_null_name.yaml @@ -0,0 +1,43 @@ +esphome: + name: scheduler-null-name + +host: + +logger: + level: DEBUG + +api: + services: + - service: test_null_name + then: + - lambda: |- + // First, create a scenario that would trigger the crash + // The crash happens when defer() is called with a name that would be cancelled + + // Test 1: Create a defer with a valid name + App.scheduler.set_timeout(nullptr, "test_defer", 0, []() { + ESP_LOGI("TEST", "First defer should be cancelled"); + }); + + // Test 2: Create another defer with the same name - this triggers cancel_item_locked_ + // In the unfixed code, this would crash if the name was NULL + App.scheduler.set_timeout(nullptr, "test_defer", 0, []() { + ESP_LOGI("TEST", "Second defer executed"); + }); + + // Test 3: Now test with nullptr - this is the actual crash scenario + // Create a defer item without a name (like voice assistant does) + const char* null_name = nullptr; + App.scheduler.set_timeout(nullptr, null_name, 0, []() { + ESP_LOGI("TEST", "Defer with null name executed"); + }); + + // Test 4: Create another defer with null name - this would trigger the crash + App.scheduler.set_timeout(nullptr, null_name, 0, []() { + ESP_LOGI("TEST", "Second null defer executed"); + }); + + // Test 5: Verify scheduler still works + App.scheduler.set_timeout(nullptr, "valid_timeout", 50, []() { + ESP_LOGI("TEST", "Test completed successfully"); + }); diff --git a/tests/integration/fixtures/scheduler_retry_test.yaml b/tests/integration/fixtures/scheduler_retry_test.yaml new file mode 100644 index 0000000000..1b6ee8e5c2 --- /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, "", 100, 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 150ms + App.scheduler.set_timeout(component, "empty_cancel_timer", 150, []() { + 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_custom_services.py b/tests/integration/test_api_custom_services.py new file mode 100644 index 0000000000..2862dcc900 --- /dev/null +++ b/tests/integration/test_api_custom_services.py @@ -0,0 +1,144 @@ +"""Integration test for API custom services using CustomAPIDevice.""" + +from __future__ import annotations + +import asyncio +from pathlib import Path +import re + +from aioesphomeapi import UserService, UserServiceArgType +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_custom_services( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test CustomAPIDevice services work correctly with custom_services: true.""" + # Get the path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + loop = asyncio.get_running_loop() + + # Track log messages + yaml_service_future = loop.create_future() + custom_service_future = loop.create_future() + custom_args_future = loop.create_future() + custom_arrays_future = loop.create_future() + + # Patterns to match in logs + yaml_service_pattern = re.compile(r"YAML service called") + custom_service_pattern = re.compile(r"Custom test service called!") + custom_args_pattern = re.compile( + r"Custom service called with: test_string, 456, 1, 78\.90" + ) + custom_arrays_pattern = re.compile( + r"Array service called with 2 bools, 3 ints, 2 floats, 2 strings" + ) + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if not yaml_service_future.done() and yaml_service_pattern.search(line): + yaml_service_future.set_result(True) + elif not custom_service_future.done() and custom_service_pattern.search(line): + custom_service_future.set_result(True) + elif not custom_args_future.done() and custom_args_pattern.search(line): + custom_args_future.set_result(True) + elif not custom_arrays_future.done() and custom_arrays_pattern.search(line): + custom_arrays_future.set_result(True) + + # Run with log monitoring + async with run_compiled(yaml_config, line_callback=check_output): + async with 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-custom-services-test" + + # List services + _, services = await client.list_entities_services() + + # Should have 4 services: 1 YAML + 3 CustomAPIDevice + assert len(services) == 4, f"Expected 4 services, found {len(services)}" + + # Find our services + yaml_service: UserService | None = None + custom_service: UserService | None = None + custom_args_service: UserService | None = None + custom_arrays_service: UserService | None = None + + for service in services: + if service.name == "test_yaml_service": + yaml_service = service + elif service.name == "custom_test_service": + custom_service = service + elif service.name == "custom_service_with_args": + custom_args_service = service + elif service.name == "custom_service_with_arrays": + custom_arrays_service = service + + assert yaml_service is not None, "test_yaml_service not found" + assert custom_service is not None, "custom_test_service not found" + assert custom_args_service is not None, "custom_service_with_args not found" + assert custom_arrays_service is not None, ( + "custom_service_with_arrays not found" + ) + + # Test YAML service + client.execute_service(yaml_service, {}) + await asyncio.wait_for(yaml_service_future, timeout=5.0) + + # Test simple CustomAPIDevice service + client.execute_service(custom_service, {}) + await asyncio.wait_for(custom_service_future, timeout=5.0) + + # Verify custom_args_service arguments + assert len(custom_args_service.args) == 4 + arg_types = {arg.name: arg.type for arg in custom_args_service.args} + assert arg_types["arg_string"] == UserServiceArgType.STRING + assert arg_types["arg_int"] == UserServiceArgType.INT + assert arg_types["arg_bool"] == UserServiceArgType.BOOL + assert arg_types["arg_float"] == UserServiceArgType.FLOAT + + # Test CustomAPIDevice service with arguments + client.execute_service( + custom_args_service, + { + "arg_string": "test_string", + "arg_int": 456, + "arg_bool": True, + "arg_float": 78.9, + }, + ) + await asyncio.wait_for(custom_args_future, timeout=5.0) + + # Verify array service arguments + assert len(custom_arrays_service.args) == 4 + array_arg_types = {arg.name: arg.type for arg in custom_arrays_service.args} + assert array_arg_types["bool_array"] == UserServiceArgType.BOOL_ARRAY + assert array_arg_types["int_array"] == UserServiceArgType.INT_ARRAY + assert array_arg_types["float_array"] == UserServiceArgType.FLOAT_ARRAY + assert array_arg_types["string_array"] == UserServiceArgType.STRING_ARRAY + + # Test CustomAPIDevice service with arrays + client.execute_service( + custom_arrays_service, + { + "bool_array": [True, False], + "int_array": [1, 2, 3], + "float_array": [1.1, 2.2], + "string_array": ["hello", "world"], + }, + ) + await asyncio.wait_for(custom_arrays_future, timeout=5.0) 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 d5f868aa93..2a866b1574 100644 --- a/tests/integration/test_loop_disable_enable.py +++ b/tests/integration/test_loop_disable_enable.py @@ -45,11 +45,18 @@ async def test_loop_disable_enable( isr_component_disabled = asyncio.Event() isr_component_re_enabled = asyncio.Event() isr_component_pure_re_enabled = asyncio.Event() + # Events for update component testing + update_component_loop_disabled = asyncio.Event() + update_component_manual_update_called = asyncio.Event() # Track loop counts for components self_disable_10_counts: list[int] = [] normal_component_counts: list[int] = [] isr_component_counts: list[int] = [] + # Track update component behavior + update_component_loop_count = 0 + update_component_update_count = 0 + update_component_manual_update_count = 0 def on_log_line(line: str) -> None: """Process each log line from the process output.""" @@ -59,6 +66,7 @@ async def test_loop_disable_enable( if ( "loop_test_component" not in clean_line and "loop_test_isr_component" not in clean_line + and "Manually calling component.update" not in clean_line ): return @@ -112,6 +120,23 @@ async def test_loop_disable_enable( elif "Running after pure ISR re-enable!" in clean_line: isr_component_pure_re_enabled.set() + # Update component events + elif "[update_test]" in clean_line: + if "LoopTestUpdateComponent loop count:" in clean_line: + nonlocal update_component_loop_count + update_component_loop_count = int( + clean_line.split("LoopTestUpdateComponent loop count: ")[1] + ) + elif "LoopTestUpdateComponent update() called" in clean_line: + nonlocal update_component_update_count + update_component_update_count += 1 + if "Manually calling component.update" in " ".join(log_messages[-5:]): + nonlocal update_component_manual_update_count + update_component_manual_update_count += 1 + update_component_manual_update_called.set() + elif "Disabling loop after" in clean_line: + update_component_loop_disabled.set() + # Write, compile and run the ESPHome device with log callback async with ( run_compiled(yaml_config, line_callback=on_log_line), @@ -125,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 @@ -139,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)}" ) @@ -147,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" ) @@ -160,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 @@ -173,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 @@ -185,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 @@ -197,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 @@ -205,3 +230,28 @@ async def test_loop_disable_enable( assert final_count > 10, ( f"Component didn't run after pure ISR enable: got {final_count} counts total" ) + + # Test component.update functionality when loop is disabled + # Wait for update component to disable its loop + try: + await asyncio.wait_for(update_component_loop_disabled.wait(), timeout=3.0) + except TimeoutError: + pytest.fail("Update component did not disable its loop within 3 seconds") + + # Verify it ran exactly 3 loops before disabling + assert update_component_loop_count == 3, ( + f"Expected 3 loop iterations before disable, got {update_component_loop_count}" + ) + + # Wait for manual component.update to be called + try: + await asyncio.wait_for( + update_component_manual_update_called.wait(), timeout=5.0 + ) + 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 + assert update_component_manual_update_count >= 1, ( + "component.update did not fire 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 new file mode 100644 index 0000000000..66e25d4a11 --- /dev/null +++ b/tests/integration/test_scheduler_null_name.py @@ -0,0 +1,59 @@ +"""Test that scheduler handles NULL names safely without crashing.""" + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_null_name( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that scheduler handles NULL names safely without crashing.""" + + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[bool] = loop.create_future() + + # Pattern to match test completion + test_complete_pattern = re.compile(r"Test completed successfully") + + def check_output(line: str) -> None: + """Check log output for test completion.""" + if not test_complete_future.done() and test_complete_pattern.search(line): + test_complete_future.set_result(True) + + async with run_compiled(yaml_config, line_callback=check_output): + async with 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-null-name" + + # List services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find our test service + test_null_name_service = next( + (s for s in services if s.name == "test_null_name"), None + ) + assert test_null_name_service is not None, ( + "test_null_name service not found" + ) + + # Execute the test + client.execute_service(test_null_name_service, {}) + + # Wait for test completion + try: + await asyncio.wait_for(test_complete_future, timeout=10.0) + 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/__init__.py b/tests/script/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/script/test_clang_tidy_hash.py b/tests/script/test_clang_tidy_hash.py new file mode 100644 index 0000000000..7b66a69adb --- /dev/null +++ b/tests/script/test_clang_tidy_hash.py @@ -0,0 +1,328 @@ +"""Unit tests for script/clang_tidy_hash.py module.""" + +import hashlib +from pathlib import Path +import sys +from unittest.mock import Mock, patch + +import pytest + +# Add the script directory to Python path so we can import clang_tidy_hash +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "script")) + +import clang_tidy_hash # noqa: E402 + + +@pytest.mark.parametrize( + ("file_content", "expected"), + [ + ( + "clang-tidy==18.1.5 # via -r requirements_dev.in\n", + "clang-tidy==18.1.5 # via -r requirements_dev.in", + ), + ( + "other-package==1.0\nclang-tidy==17.0.0\nmore-packages==2.0\n", + "clang-tidy==17.0.0", + ), + ( + "# comment\nclang-tidy==16.0.0 # some comment\n", + "clang-tidy==16.0.0 # some comment", + ), + ("no-clang-tidy-here==1.0\n", "clang-tidy version not found"), + ], +) +def test_get_clang_tidy_version_from_requirements( + file_content: str, expected: str +) -> None: + """Test extracting clang-tidy version from various file formats.""" + # Mock read_file_lines to return our test content + with patch("clang_tidy_hash.read_file_lines") as mock_read: + mock_read.return_value = file_content.splitlines(keepends=True) + + result = clang_tidy_hash.get_clang_tidy_version_from_requirements() + + assert result == expected + + +def test_calculate_clang_tidy_hash() -> None: + """Test calculating hash from all configuration sources.""" + clang_tidy_content = b"Checks: '-*,readability-*'\n" + requirements_version = "clang-tidy==18.1.5" + platformio_content = b"[env:esp32]\nplatform = espressif32\n" + + # Expected hash calculation + expected_hasher = hashlib.sha256() + expected_hasher.update(clang_tidy_content) + expected_hasher.update(requirements_version.encode()) + expected_hasher.update(platformio_content) + expected_hash = expected_hasher.hexdigest() + + # Mock the dependencies + with ( + patch("clang_tidy_hash.read_file_bytes") as mock_read_bytes, + patch( + "clang_tidy_hash.get_clang_tidy_version_from_requirements", + return_value=requirements_version, + ), + ): + # Set up mock to return different content based on the file being read + def read_file_mock(path: Path) -> bytes: + if ".clang-tidy" in str(path): + return clang_tidy_content + elif "platformio.ini" in str(path): + return platformio_content + return b"" + + mock_read_bytes.side_effect = read_file_mock + result = clang_tidy_hash.calculate_clang_tidy_hash() + + assert result == expected_hash + + +def test_read_stored_hash_exists(tmp_path: Path) -> None: + """Test reading hash when file exists.""" + stored_hash = "abc123def456" + hash_file = tmp_path / ".clang-tidy.hash" + hash_file.write_text(f"{stored_hash}\n") + + with ( + patch("clang_tidy_hash.Path") as mock_path_class, + patch("clang_tidy_hash.read_file_lines", return_value=[f"{stored_hash}\n"]), + ): + # Mock the path calculation and exists check + mock_hash_file = Mock() + mock_hash_file.exists.return_value = True + mock_path_class.return_value.parent.parent.__truediv__.return_value = ( + mock_hash_file + ) + + result = clang_tidy_hash.read_stored_hash() + + assert result == stored_hash + + +def test_read_stored_hash_not_exists() -> None: + """Test reading hash when file doesn't exist.""" + with patch("clang_tidy_hash.Path") as mock_path_class: + # Mock the path calculation and exists check + mock_hash_file = Mock() + mock_hash_file.exists.return_value = False + mock_path_class.return_value.parent.parent.__truediv__.return_value = ( + mock_hash_file + ) + + result = clang_tidy_hash.read_stored_hash() + + assert result is None + + +def test_write_hash() -> None: + """Test writing hash to file.""" + hash_value = "abc123def456" + + with patch("clang_tidy_hash.write_file_content") as mock_write: + clang_tidy_hash.write_hash(hash_value) + + # Verify write_file_content was called with correct parameters + mock_write.assert_called_once() + args = mock_write.call_args[0] + assert str(args[0]).endswith(".clang-tidy.hash") + assert args[1] == hash_value.strip() + "\n" + + +@pytest.mark.parametrize( + ("args", "current_hash", "stored_hash", "expected_exit"), + [ + (["--check"], "abc123", "abc123", 1), # Hashes match, no scan needed + (["--check"], "abc123", "def456", 0), # Hashes differ, scan needed + (["--check"], "abc123", None, 0), # No stored hash, scan needed + ], +) +def test_main_check_mode( + args: list[str], current_hash: str, stored_hash: str | None, expected_exit: int +) -> None: + """Test main function in check mode.""" + with ( + patch("sys.argv", ["clang_tidy_hash.py"] + args), + patch("clang_tidy_hash.calculate_clang_tidy_hash", return_value=current_hash), + patch("clang_tidy_hash.read_stored_hash", return_value=stored_hash), + pytest.raises(SystemExit) as exc_info, + ): + clang_tidy_hash.main() + + assert exc_info.value.code == expected_exit + + +def test_main_update_mode(capsys: pytest.CaptureFixture[str]) -> None: + """Test main function in update mode.""" + current_hash = "abc123" + + with ( + patch("sys.argv", ["clang_tidy_hash.py", "--update"]), + patch("clang_tidy_hash.calculate_clang_tidy_hash", return_value=current_hash), + patch("clang_tidy_hash.write_hash") as mock_write, + ): + clang_tidy_hash.main() + + mock_write.assert_called_once_with(current_hash) + captured = capsys.readouterr() + assert f"Hash updated: {current_hash}" in captured.out + + +@pytest.mark.parametrize( + ("current_hash", "stored_hash"), + [ + ("abc123", "def456"), # Hash changed, should update + ("abc123", None), # No stored hash, should update + ], +) +def test_main_update_if_changed_mode_update( + current_hash: str, stored_hash: str | None, capsys: pytest.CaptureFixture[str] +) -> None: + """Test main function in update-if-changed mode when update is needed.""" + with ( + patch("sys.argv", ["clang_tidy_hash.py", "--update-if-changed"]), + patch("clang_tidy_hash.calculate_clang_tidy_hash", return_value=current_hash), + patch("clang_tidy_hash.read_stored_hash", return_value=stored_hash), + patch("clang_tidy_hash.write_hash") as mock_write, + pytest.raises(SystemExit) as exc_info, + ): + clang_tidy_hash.main() + + assert exc_info.value.code == 0 + mock_write.assert_called_once_with(current_hash) + captured = capsys.readouterr() + assert "Clang-tidy hash updated" in captured.out + + +def test_main_update_if_changed_mode_no_update( + capsys: pytest.CaptureFixture[str], +) -> None: + """Test main function in update-if-changed mode when no update is needed.""" + current_hash = "abc123" + stored_hash = "abc123" + + with ( + patch("sys.argv", ["clang_tidy_hash.py", "--update-if-changed"]), + patch("clang_tidy_hash.calculate_clang_tidy_hash", return_value=current_hash), + patch("clang_tidy_hash.read_stored_hash", return_value=stored_hash), + patch("clang_tidy_hash.write_hash") as mock_write, + pytest.raises(SystemExit) as exc_info, + ): + clang_tidy_hash.main() + + assert exc_info.value.code == 0 + mock_write.assert_not_called() + captured = capsys.readouterr() + assert "Clang-tidy hash unchanged" in captured.out + + +def test_main_verify_mode_success(capsys: pytest.CaptureFixture[str]) -> None: + """Test main function in verify mode when verification passes.""" + current_hash = "abc123" + stored_hash = "abc123" + + with ( + patch("sys.argv", ["clang_tidy_hash.py", "--verify"]), + patch("clang_tidy_hash.calculate_clang_tidy_hash", return_value=current_hash), + patch("clang_tidy_hash.read_stored_hash", return_value=stored_hash), + ): + clang_tidy_hash.main() + captured = capsys.readouterr() + assert "Hash verification passed" in captured.out + + +@pytest.mark.parametrize( + ("current_hash", "stored_hash"), + [ + ("abc123", "def456"), # Hashes differ, verification fails + ("abc123", None), # No stored hash, verification fails + ], +) +def test_main_verify_mode_failure( + current_hash: str, stored_hash: str | None, capsys: pytest.CaptureFixture[str] +) -> None: + """Test main function in verify mode when verification fails.""" + with ( + patch("sys.argv", ["clang_tidy_hash.py", "--verify"]), + patch("clang_tidy_hash.calculate_clang_tidy_hash", return_value=current_hash), + patch("clang_tidy_hash.read_stored_hash", return_value=stored_hash), + pytest.raises(SystemExit) as exc_info, + ): + clang_tidy_hash.main() + + assert exc_info.value.code == 1 + captured = capsys.readouterr() + assert "ERROR: Clang-tidy configuration has changed" in captured.out + + +def test_main_default_mode(capsys: pytest.CaptureFixture[str]) -> None: + """Test main function in default mode (no arguments).""" + current_hash = "abc123" + stored_hash = "def456" + + with ( + patch("sys.argv", ["clang_tidy_hash.py"]), + patch("clang_tidy_hash.calculate_clang_tidy_hash", return_value=current_hash), + patch("clang_tidy_hash.read_stored_hash", return_value=stored_hash), + ): + clang_tidy_hash.main() + + captured = capsys.readouterr() + assert f"Current hash: {current_hash}" in captured.out + assert f"Stored hash: {stored_hash}" in captured.out + assert "Match: False" in captured.out + + +def test_read_file_lines(tmp_path: Path) -> None: + """Test read_file_lines helper function.""" + test_file = tmp_path / "test.txt" + test_content = "line1\nline2\nline3\n" + test_file.write_text(test_content) + + result = clang_tidy_hash.read_file_lines(test_file) + + assert result == ["line1\n", "line2\n", "line3\n"] + + +def test_read_file_bytes(tmp_path: Path) -> None: + """Test read_file_bytes helper function.""" + test_file = tmp_path / "test.bin" + test_content = b"binary content\x00\xff" + test_file.write_bytes(test_content) + + result = clang_tidy_hash.read_file_bytes(test_file) + + assert result == test_content + + +def test_write_file_content(tmp_path: Path) -> None: + """Test write_file_content helper function.""" + test_file = tmp_path / "test.txt" + test_content = "test content" + + clang_tidy_hash.write_file_content(test_file, test_content) + + assert test_file.read_text() == test_content + + +@pytest.mark.parametrize( + ("line", "expected"), + [ + ("clang-tidy==18.1.5", ("clang-tidy", "clang-tidy==18.1.5")), + ( + "clang-tidy==18.1.5 # comment", + ("clang-tidy", "clang-tidy==18.1.5 # comment"), + ), + ("some-package>=1.0,<2.0", ("some-package", "some-package>=1.0,<2.0")), + ("pkg_with-dashes==1.0", ("pkg_with-dashes", "pkg_with-dashes==1.0")), + ("# just a comment", None), + ("", None), + (" ", None), + ("invalid line without version", None), + ], +) +def test_parse_requirement_line(line: str, expected: tuple[str, str] | None) -> None: + """Test parsing individual requirement lines.""" + result = clang_tidy_hash.parse_requirement_line(line) + assert result == expected diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py new file mode 100644 index 0000000000..84be7344c3 --- /dev/null +++ b/tests/script/test_determine_jobs.py @@ -0,0 +1,371 @@ +"""Unit tests for script/determine-jobs.py module.""" + +from collections.abc import Generator +import importlib.util +import json +import os +import subprocess +import sys +from unittest.mock import Mock, call, patch + +import pytest + +# Add the script directory to Python path so we can import the module +script_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "script") +) +sys.path.insert(0, script_dir) + +spec = importlib.util.spec_from_file_location( + "determine_jobs", os.path.join(script_dir, "determine-jobs.py") +) +determine_jobs = importlib.util.module_from_spec(spec) +spec.loader.exec_module(determine_jobs) + + +@pytest.fixture +def mock_should_run_integration_tests() -> Generator[Mock, None, None]: + """Mock should_run_integration_tests from helpers.""" + with patch.object(determine_jobs, "should_run_integration_tests") as mock: + yield mock + + +@pytest.fixture +def mock_should_run_clang_tidy() -> Generator[Mock, None, None]: + """Mock should_run_clang_tidy from helpers.""" + with patch.object(determine_jobs, "should_run_clang_tidy") as mock: + yield mock + + +@pytest.fixture +def mock_should_run_clang_format() -> Generator[Mock, None, None]: + """Mock should_run_clang_format from helpers.""" + with patch.object(determine_jobs, "should_run_clang_format") as mock: + yield mock + + +@pytest.fixture +def mock_should_run_python_linters() -> Generator[Mock, None, None]: + """Mock should_run_python_linters from helpers.""" + with patch.object(determine_jobs, "should_run_python_linters") as mock: + yield mock + + +@pytest.fixture +def mock_subprocess_run() -> Generator[Mock, None, None]: + """Mock subprocess.run for list-components.py calls.""" + with patch.object(determine_jobs.subprocess, "run") as mock: + yield mock + + +def test_main_all_tests_should_run( + mock_should_run_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_subprocess_run: Mock, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test when all tests should run.""" + mock_should_run_integration_tests.return_value = True + mock_should_run_clang_tidy.return_value = True + mock_should_run_clang_format.return_value = True + mock_should_run_python_linters.return_value = True + + # Mock list-components.py output + mock_result = Mock() + mock_result.stdout = "wifi\napi\nsensor\n" + mock_subprocess_run.return_value = mock_result + + # Run main function with mocked argv + with patch("sys.argv", ["determine-jobs.py"]): + determine_jobs.main() + + # Check output + captured = capsys.readouterr() + output = json.loads(captured.out) + + assert output["integration_tests"] is True + assert output["clang_tidy"] is True + assert output["clang_format"] is True + assert output["python_linters"] is True + assert output["changed_components"] == ["wifi", "api", "sensor"] + assert output["component_test_count"] == 3 + + +def test_main_no_tests_should_run( + mock_should_run_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_subprocess_run: Mock, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test when no tests should run.""" + mock_should_run_integration_tests.return_value = False + mock_should_run_clang_tidy.return_value = False + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = False + + # Mock empty list-components.py output + mock_result = Mock() + mock_result.stdout = "" + mock_subprocess_run.return_value = mock_result + + # Run main function with mocked argv + with patch("sys.argv", ["determine-jobs.py"]): + determine_jobs.main() + + # Check output + captured = capsys.readouterr() + output = json.loads(captured.out) + + assert output["integration_tests"] is False + assert output["clang_tidy"] is False + assert output["clang_format"] is False + assert output["python_linters"] is False + assert output["changed_components"] == [] + assert output["component_test_count"] == 0 + + +def test_main_list_components_fails( + mock_should_run_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_subprocess_run: Mock, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test when list-components.py fails.""" + mock_should_run_integration_tests.return_value = True + mock_should_run_clang_tidy.return_value = True + mock_should_run_clang_format.return_value = True + mock_should_run_python_linters.return_value = True + + # Mock list-components.py failure + mock_subprocess_run.side_effect = subprocess.CalledProcessError(1, "cmd") + + # Run main function with mocked argv - should raise + with patch("sys.argv", ["determine-jobs.py"]): + with pytest.raises(subprocess.CalledProcessError): + determine_jobs.main() + + +def test_main_with_branch_argument( + mock_should_run_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_subprocess_run: Mock, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test with branch argument.""" + mock_should_run_integration_tests.return_value = False + mock_should_run_clang_tidy.return_value = True + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = True + + # Mock list-components.py output + mock_result = Mock() + mock_result.stdout = "mqtt\n" + mock_subprocess_run.return_value = mock_result + + with patch("sys.argv", ["script.py", "-b", "main"]): + determine_jobs.main() + + # Check that functions were called with branch + mock_should_run_integration_tests.assert_called_once_with("main") + mock_should_run_clang_tidy.assert_called_once_with("main") + mock_should_run_clang_format.assert_called_once_with("main") + mock_should_run_python_linters.assert_called_once_with("main") + + # Check that list-components.py was called with branch + mock_subprocess_run.assert_called_once() + call_args = mock_subprocess_run.call_args[0][0] + assert "--changed" in call_args + assert "-b" in call_args + assert "main" in call_args + + # Check output + captured = capsys.readouterr() + output = json.loads(captured.out) + + assert output["integration_tests"] is False + assert output["clang_tidy"] is True + assert output["clang_format"] is False + assert output["python_linters"] is True + assert output["changed_components"] == ["mqtt"] + assert output["component_test_count"] == 1 + + +def test_should_run_integration_tests( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test should_run_integration_tests function.""" + # Core C++ files trigger tests + with patch.object( + determine_jobs, "changed_files", return_value=["esphome/core/component.cpp"] + ): + result = determine_jobs.should_run_integration_tests() + assert result is True + + # Core Python files trigger tests + with patch.object( + determine_jobs, "changed_files", return_value=["esphome/core/config.py"] + ): + result = determine_jobs.should_run_integration_tests() + assert result is True + + # Python files directly in esphome/ do NOT trigger tests + with patch.object( + determine_jobs, "changed_files", return_value=["esphome/config.py"] + ): + result = determine_jobs.should_run_integration_tests() + assert result is False + + # Python files in subdirectories (not core) do NOT trigger tests + with patch.object( + determine_jobs, + "changed_files", + return_value=["esphome/dashboard/web_server.py"], + ): + result = determine_jobs.should_run_integration_tests() + assert result is False + + +def test_should_run_integration_tests_with_branch() -> None: + """Test should_run_integration_tests with branch argument.""" + with patch.object(determine_jobs, "changed_files") as mock_changed: + mock_changed.return_value = [] + determine_jobs.should_run_integration_tests("release") + mock_changed.assert_called_once_with("release") + + +def test_should_run_integration_tests_component_dependency() -> None: + """Test that integration tests run when components used in fixtures change.""" + with patch.object( + determine_jobs, "changed_files", return_value=["esphome/components/api/api.cpp"] + ): + with patch.object( + determine_jobs, "get_components_from_integration_fixtures" + ) as mock_fixtures: + mock_fixtures.return_value = {"api", "sensor"} + with patch.object(determine_jobs, "get_all_dependencies") as mock_deps: + mock_deps.return_value = {"api", "sensor", "network"} + result = determine_jobs.should_run_integration_tests() + assert result is True + + +@pytest.mark.parametrize( + ("check_returncode", "changed_files", "expected_result"), + [ + (0, [], True), # Hash changed - need full scan + (1, ["esphome/core.cpp"], True), # C++ file changed + (1, ["README.md"], False), # No C++ files changed + (1, [".clang-tidy.hash"], True), # Hash file itself changed + (1, ["platformio.ini", ".clang-tidy.hash"], True), # Config + hash changed + ], +) +def test_should_run_clang_tidy( + check_returncode: int, + changed_files: list[str], + expected_result: bool, +) -> None: + """Test should_run_clang_tidy function.""" + with patch.object(determine_jobs, "changed_files", return_value=changed_files): + # Test with hash check returning specific code + with patch("subprocess.run") as mock_run: + mock_run.return_value = Mock(returncode=check_returncode) + result = determine_jobs.should_run_clang_tidy() + assert result == expected_result + + +def test_should_run_clang_tidy_hash_check_exception() -> None: + """Test should_run_clang_tidy when hash check fails with exception.""" + # When hash check fails, clang-tidy should run as a safety measure + with ( + patch.object(determine_jobs, "changed_files", return_value=["README.md"]), + patch("subprocess.run", side_effect=Exception("Hash check failed")), + ): + result = determine_jobs.should_run_clang_tidy() + assert result is True # Fail safe - run clang-tidy + + # Even with C++ files, exception should trigger clang-tidy + with ( + patch.object( + determine_jobs, "changed_files", return_value=["esphome/core.cpp"] + ), + patch("subprocess.run", side_effect=Exception("Hash check failed")), + ): + result = determine_jobs.should_run_clang_tidy() + assert result is True + + +def test_should_run_clang_tidy_with_branch() -> None: + """Test should_run_clang_tidy with branch argument.""" + with patch.object(determine_jobs, "changed_files") as mock_changed: + mock_changed.return_value = [] + with patch("subprocess.run") as mock_run: + mock_run.return_value = Mock(returncode=1) # Hash unchanged + determine_jobs.should_run_clang_tidy("release") + # Changed files is called twice now - once for hash check, once for .clang-tidy.hash check + assert mock_changed.call_count == 2 + mock_changed.assert_has_calls([call("release"), call("release")]) + + +@pytest.mark.parametrize( + ("changed_files", "expected_result"), + [ + (["esphome/core.py"], True), + (["script/test.py"], True), + (["esphome/test.pyi"], True), # .pyi files should trigger + (["README.md"], False), + ([], False), + ], +) +def test_should_run_python_linters( + changed_files: list[str], expected_result: bool +) -> None: + """Test should_run_python_linters function.""" + with patch.object(determine_jobs, "changed_files", return_value=changed_files): + result = determine_jobs.should_run_python_linters() + assert result == expected_result + + +def test_should_run_python_linters_with_branch() -> None: + """Test should_run_python_linters with branch argument.""" + with patch.object(determine_jobs, "changed_files") as mock_changed: + mock_changed.return_value = [] + determine_jobs.should_run_python_linters("release") + mock_changed.assert_called_once_with("release") + + +@pytest.mark.parametrize( + ("changed_files", "expected_result"), + [ + (["esphome/core.cpp"], True), + (["esphome/core.h"], True), + (["test.hpp"], True), + (["test.cc"], True), + (["test.cxx"], True), + (["test.c"], True), + (["test.tcc"], True), + (["README.md"], False), + ([], False), + ], +) +def test_should_run_clang_format( + changed_files: list[str], expected_result: bool +) -> None: + """Test should_run_clang_format function.""" + with patch.object(determine_jobs, "changed_files", return_value=changed_files): + result = determine_jobs.should_run_clang_format() + assert result == expected_result + + +def test_should_run_clang_format_with_branch() -> None: + """Test should_run_clang_format with branch argument.""" + with patch.object(determine_jobs, "changed_files") as mock_changed: + mock_changed.return_value = [] + determine_jobs.should_run_clang_format("release") + mock_changed.assert_called_once_with("release") diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py new file mode 100644 index 0000000000..423e2d3c30 --- /dev/null +++ b/tests/script/test_helpers.py @@ -0,0 +1,1013 @@ +"""Unit tests for script/helpers.py module.""" + +import json +import os +from pathlib import Path +import subprocess +import sys +from unittest.mock import Mock, patch + +import pytest +from pytest import MonkeyPatch + +# Add the script directory to Python path so we can import helpers +sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "script")) +) + +import helpers # noqa: E402 + +changed_files = helpers.changed_files +filter_changed = helpers.filter_changed +get_changed_components = helpers.get_changed_components +_get_changed_files_from_command = helpers._get_changed_files_from_command +_get_pr_number_from_github_env = helpers._get_pr_number_from_github_env +_get_changed_files_github_actions = helpers._get_changed_files_github_actions +_filter_changed_ci = helpers._filter_changed_ci +_filter_changed_local = helpers._filter_changed_local +build_all_include = helpers.build_all_include +print_file_list = helpers.print_file_list +get_all_dependencies = helpers.get_all_dependencies + + +@pytest.mark.parametrize( + ("github_ref", "expected_pr_number"), + [ + ("refs/pull/1234/merge", "1234"), + ("refs/pull/5678/head", "5678"), + ("refs/pull/999/merge", "999"), + ("refs/heads/main", None), + ("", None), + ], +) +def test_get_pr_number_from_github_env_ref( + monkeypatch: MonkeyPatch, github_ref: str, expected_pr_number: str | None +) -> None: + """Test extracting PR number from GITHUB_REF.""" + monkeypatch.setenv("GITHUB_REF", github_ref) + # Make sure GITHUB_EVENT_PATH is not set + monkeypatch.delenv("GITHUB_EVENT_PATH", raising=False) + + result = _get_pr_number_from_github_env() + + assert result == expected_pr_number + + +def test_get_pr_number_from_github_env_event_file( + monkeypatch: MonkeyPatch, tmp_path: Path +) -> None: + """Test extracting PR number from GitHub event file.""" + # No PR number in ref + monkeypatch.setenv("GITHUB_REF", "refs/heads/feature-branch") + + event_file = tmp_path / "event.json" + event_data = {"pull_request": {"number": 5678}} + event_file.write_text(json.dumps(event_data)) + monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_file)) + + result = _get_pr_number_from_github_env() + + assert result == "5678" + + +def test_get_pr_number_from_github_env_no_pr( + monkeypatch: MonkeyPatch, tmp_path: Path +) -> None: + """Test when no PR number is available.""" + monkeypatch.setenv("GITHUB_REF", "refs/heads/main") + + event_file = tmp_path / "event.json" + event_data = {"push": {"head_commit": {"id": "abc123"}}} + event_file.write_text(json.dumps(event_data)) + monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_file)) + + result = _get_pr_number_from_github_env() + + assert result is None + + +@pytest.mark.parametrize( + ("github_ref", "expected_pr_number"), + [ + ("refs/pull/1234/merge", "1234"), + ("refs/pull/5678/head", "5678"), + ("refs/pull/999/merge", "999"), + ], +) +def test_github_actions_pull_request_with_pr_number_in_ref( + monkeypatch: MonkeyPatch, github_ref: str, expected_pr_number: str +) -> None: + """Test PR detection via GITHUB_REF.""" + monkeypatch.setenv("GITHUB_ACTIONS", "true") + monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") + monkeypatch.setenv("GITHUB_REF", github_ref) + + expected_files = ["file1.py", "file2.cpp"] + + with patch("helpers._get_changed_files_from_command") as mock_get: + mock_get.return_value = expected_files + + result = changed_files() + + mock_get.assert_called_once_with( + ["gh", "pr", "diff", expected_pr_number, "--name-only"] + ) + assert result == expected_files + + +def test_github_actions_pull_request_with_event_file( + monkeypatch: MonkeyPatch, tmp_path: Path +) -> None: + """Test PR detection via GitHub event file.""" + monkeypatch.setenv("GITHUB_ACTIONS", "true") + monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") + monkeypatch.setenv("GITHUB_REF", "refs/heads/feature-branch") + + event_file = tmp_path / "event.json" + event_data = {"pull_request": {"number": 5678}} + event_file.write_text(json.dumps(event_data)) + monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_file)) + + expected_files = ["file1.py", "file2.cpp"] + + with patch("helpers._get_changed_files_from_command") as mock_get: + mock_get.return_value = expected_files + + result = changed_files() + + mock_get.assert_called_once_with(["gh", "pr", "diff", "5678", "--name-only"]) + assert result == expected_files + + +def test_github_actions_push_event(monkeypatch: MonkeyPatch) -> None: + """Test push event handling.""" + monkeypatch.setenv("GITHUB_ACTIONS", "true") + monkeypatch.setenv("GITHUB_EVENT_NAME", "push") + + expected_files = ["file1.py", "file2.cpp"] + + with patch("helpers._get_changed_files_from_command") as mock_get: + mock_get.return_value = expected_files + + result = changed_files() + + mock_get.assert_called_once_with(["git", "diff", "HEAD~1..HEAD", "--name-only"]) + assert result == expected_files + + +@pytest.fixture(autouse=True) +def clear_caches(): + """Clear function caches before each test.""" + # Clear the cache for _get_changed_files_github_actions + _get_changed_files_github_actions.cache_clear() + yield + + +def test_get_changed_files_github_actions_pull_request( + monkeypatch: MonkeyPatch, +) -> None: + """Test _get_changed_files_github_actions for pull request event.""" + monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") + + expected_files = ["file1.py", "file2.cpp"] + + with ( + patch("helpers._get_pr_number_from_github_env", return_value="1234"), + patch("helpers._get_changed_files_from_command") as mock_get, + ): + mock_get.return_value = expected_files + + result = _get_changed_files_github_actions() + + mock_get.assert_called_once_with(["gh", "pr", "diff", "1234", "--name-only"]) + assert result == expected_files + + +def test_get_changed_files_github_actions_pull_request_no_pr_number( + monkeypatch: MonkeyPatch, +) -> None: + """Test _get_changed_files_github_actions when no PR number is found.""" + monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") + + with patch("helpers._get_pr_number_from_github_env", return_value=None): + result = _get_changed_files_github_actions() + + assert result is None + + +def test_get_changed_files_github_actions_push(monkeypatch: MonkeyPatch) -> None: + """Test _get_changed_files_github_actions for push event.""" + monkeypatch.setenv("GITHUB_EVENT_NAME", "push") + + expected_files = ["file1.py", "file2.cpp"] + + with patch("helpers._get_changed_files_from_command") as mock_get: + mock_get.return_value = expected_files + + result = _get_changed_files_github_actions() + + mock_get.assert_called_once_with(["git", "diff", "HEAD~1..HEAD", "--name-only"]) + assert result == expected_files + + +def test_get_changed_files_github_actions_push_fallback( + monkeypatch: MonkeyPatch, +) -> None: + """Test _get_changed_files_github_actions fallback for push event.""" + monkeypatch.setenv("GITHUB_EVENT_NAME", "push") + + with patch("helpers._get_changed_files_from_command") as mock_get: + mock_get.side_effect = Exception("Failed") + + result = _get_changed_files_github_actions() + + assert result is None + + +def test_get_changed_files_github_actions_other_event(monkeypatch: MonkeyPatch) -> None: + """Test _get_changed_files_github_actions for other event types.""" + monkeypatch.setenv("GITHUB_EVENT_NAME", "workflow_dispatch") + + result = _get_changed_files_github_actions() + + assert result is None + + +def test_github_actions_push_event_fallback(monkeypatch: MonkeyPatch) -> None: + """Test push event fallback to git merge-base.""" + monkeypatch.setenv("GITHUB_ACTIONS", "true") + monkeypatch.setenv("GITHUB_EVENT_NAME", "push") + + expected_files = ["file1.py", "file2.cpp"] + + with ( + patch("helpers._get_changed_files_from_command") as mock_get, + patch("helpers.get_output") as mock_output, + ): + # First call fails, triggering fallback + mock_get.side_effect = [ + Exception("Failed"), + expected_files, + ] + + mock_output.side_effect = [ + "origin\nupstream\n", # git remote + "abc123\n", # merge base + ] + + result = changed_files() + + assert mock_get.call_count == 2 + assert result == expected_files + + +@pytest.mark.parametrize( + ("branch", "merge_base"), + [ + (None, "abc123"), # Default branch (dev) + ("release", "def456"), + ("beta", "ghi789"), + ], +) +def test_local_development_branches( + monkeypatch: MonkeyPatch, branch: str | None, merge_base: str +) -> None: + """Test local development with different branches.""" + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + expected_files = ["file1.py", "file2.cpp"] + + with ( + patch("helpers.get_output") as mock_output, + patch("helpers._get_changed_files_from_command") as mock_get, + ): + if branch is None: + # For default branch, helpers.get_output is called twice (git remote and merge-base) + mock_output.side_effect = [ + "origin\nupstream\n", # git remote + f"{merge_base}\n", # merge base for upstream/dev + ] + else: + # For custom branch, may need more calls if trying multiple remotes + mock_output.side_effect = [ + "origin\nupstream\n", # git remote + Exception("not found"), # upstream/{branch} may fail + f"{merge_base}\n", # merge base for origin/{branch} + ] + + mock_get.return_value = expected_files + + result = changed_files(branch) + + mock_get.assert_called_once_with(["git", "diff", merge_base, "--name-only"]) + assert result == expected_files + + +def test_local_development_no_remotes_configured(monkeypatch: MonkeyPatch) -> None: + """Test error when no git remotes are configured.""" + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + with patch("helpers.get_output") as mock_output: + # The function calls get_output multiple times: + # 1. First to get list of remotes: git remote + # 2. Then for each remote it tries: git merge-base + # We simulate having some remotes but all merge-base attempts fail + def side_effect_func(*args): + if args == ("git", "remote"): + return "origin\nupstream\n" + else: + # All merge-base attempts fail + raise Exception("Command failed") + + mock_output.side_effect = side_effect_func + + with pytest.raises(ValueError, match="Git not configured"): + changed_files() + + +@pytest.mark.parametrize( + ("stdout", "expected"), + [ + ("file1.py\nfile2.cpp\n\n", ["file1.py", "file2.cpp"]), + ("\n\n", []), + ("single.py\n", ["single.py"]), + ( + "path/to/file.cpp\nanother/path.h\n", + ["another/path.h", "path/to/file.cpp"], + ), # Sorted + ], +) +def test_get_changed_files_from_command_successful( + stdout: str, expected: list[str] +) -> None: + """Test successful command execution with various outputs.""" + mock_result = Mock() + mock_result.returncode = 0 + mock_result.stdout = stdout + + with patch("subprocess.run", return_value=mock_result): + result = _get_changed_files_from_command(["git", "diff"]) + + # Normalize paths to forward slashes for comparison + # since os.path.relpath returns OS-specific separators + normalized_result = [f.replace(os.sep, "/") for f in result] + assert normalized_result == expected + + +@pytest.mark.parametrize( + ("returncode", "stderr"), + [ + (1, "Error: command failed"), + (128, "fatal: not a git repository"), + (2, "Unknown error"), + ], +) +def test_get_changed_files_from_command_failed(returncode: int, stderr: str) -> None: + """Test command failure handling.""" + mock_result = Mock() + mock_result.returncode = returncode + mock_result.stderr = stderr + + with patch("subprocess.run", return_value=mock_result): + with pytest.raises(Exception) as exc_info: + _get_changed_files_from_command(["git", "diff"]) + assert "Command failed" in str(exc_info.value) + assert stderr in str(exc_info.value) + + +def test_get_changed_files_from_command_relative_paths() -> None: + """Test that paths are made relative to current directory.""" + mock_result = Mock() + mock_result.returncode = 0 + mock_result.stdout = "/some/project/file1.py\n/some/project/sub/file2.cpp\n" + + with ( + patch("subprocess.run", return_value=mock_result), + patch( + "os.path.relpath", side_effect=["file1.py", "sub/file2.cpp"] + ) as mock_relpath, + patch("os.getcwd", return_value="/some/project"), + ): + result = _get_changed_files_from_command(["git", "diff"]) + + # Check relpath was called with correct arguments + assert mock_relpath.call_count == 2 + assert result == ["file1.py", "sub/file2.cpp"] + + +@pytest.mark.parametrize( + "changed_files_list", + [ + ["esphome/core/component.h", "esphome/components/wifi/wifi.cpp"], + ["esphome/core/helpers.cpp"], + ["esphome/core/application.h", "esphome/core/defines.h"], + ], +) +def test_get_changed_components_core_cpp_files_trigger_full_scan( + changed_files_list: list[str], +) -> None: + """Test that core C++/header file changes trigger full scan without calling subprocess.""" + with patch("helpers.changed_files") as mock_changed: + mock_changed.return_value = changed_files_list + + # Should return None without calling subprocess + result = get_changed_components() + assert result is None + + +def test_get_changed_components_core_python_files_no_full_scan() -> None: + """Test that core Python file changes do NOT trigger full scan.""" + changed_files_list = [ + "esphome/core/__init__.py", + "esphome/core/config.py", + "esphome/components/wifi/wifi.cpp", + ] + + with patch("helpers.changed_files") as mock_changed: + mock_changed.return_value = changed_files_list + + mock_result = Mock() + mock_result.stdout = "wifi\n" + + with patch("subprocess.run", return_value=mock_result): + result = get_changed_components() + # Should NOT return None - should call list-components.py + assert result == ["wifi"] + + +def test_get_changed_components_mixed_core_files_with_cpp() -> None: + """Test that mixed Python and C++ core files still trigger full scan due to C++ file.""" + changed_files_list = [ + "esphome/core/__init__.py", + "esphome/core/config.py", + "esphome/core/helpers.cpp", # This C++ file should trigger full scan + "esphome/components/wifi/wifi.cpp", + ] + + with patch("helpers.changed_files") as mock_changed: + mock_changed.return_value = changed_files_list + + # Should return None without calling subprocess due to helpers.cpp + result = get_changed_components() + assert result is None + + +@pytest.mark.parametrize( + ("changed_files_list", "expected"), + [ + # Only component files changed + ( + ["esphome/components/wifi/wifi.cpp", "esphome/components/api/api.cpp"], + ["wifi", "api"], + ), + # Non-component files only + (["README.md", "script/clang-tidy"], []), + # Single component + (["esphome/components/mqtt/mqtt_client.cpp"], ["mqtt"]), + ], +) +def test_get_changed_components_returns_component_list( + changed_files_list: list[str], expected: list[str] +) -> None: + """Test component detection returns correct component list.""" + with patch("helpers.changed_files") as mock_changed: + mock_changed.return_value = changed_files_list + + mock_result = Mock() + mock_result.stdout = "\n".join(expected) + "\n" if expected else "\n" + + with patch("subprocess.run", return_value=mock_result): + result = get_changed_components() + assert result == expected + + +def test_get_changed_components_script_failure() -> None: + """Test fallback to full scan when script fails.""" + with patch("helpers.changed_files") as mock_changed: + mock_changed.return_value = ["esphome/components/wifi/wifi_component.cpp"] + + with patch("subprocess.run") as mock_run: + mock_run.side_effect = subprocess.CalledProcessError(1, "cmd") + + result = get_changed_components() + + assert result is None # None means full scan + + +@pytest.mark.parametrize( + ("components", "all_files", "expected_files"), + [ + # Core C++/header files changed (full scan) + ( + None, + ["esphome/components/wifi/wifi.cpp", "esphome/core/helpers.cpp"], + ["esphome/components/wifi/wifi.cpp", "esphome/core/helpers.cpp"], + ), + # Specific components + ( + ["wifi", "api"], + [ + "esphome/components/wifi/wifi.cpp", + "esphome/components/wifi/wifi.h", + "esphome/components/api/api.cpp", + "esphome/components/mqtt/mqtt.cpp", + ], + [ + "esphome/components/wifi/wifi.cpp", + "esphome/components/wifi/wifi.h", + "esphome/components/api/api.cpp", + ], + ), + # No components changed + ( + [], + ["esphome/components/wifi/wifi.cpp", "script/clang-tidy"], + ["script/clang-tidy"], # Only non-component changed files + ), + ], +) +def test_filter_changed_ci_mode( + monkeypatch: MonkeyPatch, + components: list[str] | None, + all_files: list[str], + expected_files: list[str], +) -> None: + """Test filter_changed in CI mode with different component scenarios.""" + monkeypatch.setenv("GITHUB_ACTIONS", "true") + + with patch("helpers.get_changed_components") as mock_components: + mock_components.return_value = components + + if components == []: + # No components changed scenario needs changed_files mock + with patch("helpers.changed_files") as mock_changed: + mock_changed.return_value = ["script/clang-tidy", "README.md"] + result = filter_changed(all_files) + else: + result = filter_changed(all_files) + + assert set(result) == set(expected_files) + + +def test_filter_changed_local_mode(monkeypatch: MonkeyPatch) -> None: + """Test filter_changed in local mode filters files directly.""" + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + all_files = [ + "esphome/components/wifi/wifi.cpp", + "esphome/components/api/api.cpp", + "esphome/core/helpers.cpp", + ] + + with patch("helpers.changed_files") as mock_changed: + mock_changed.return_value = [ + "esphome/components/wifi/wifi.cpp", + "esphome/core/helpers.cpp", + ] + + result = filter_changed(all_files) + + # Should only include files that actually changed + expected = ["esphome/components/wifi/wifi.cpp", "esphome/core/helpers.cpp"] + assert set(result) == set(expected) + + +def test_filter_changed_component_path_parsing(monkeypatch: MonkeyPatch) -> None: + """Test correct parsing of component paths.""" + monkeypatch.setenv("GITHUB_ACTIONS", "true") + + all_files = [ + "esphome/components/wifi/wifi_component.cpp", + "esphome/components/wifi_info/wifi_info_text_sensor.cpp", # Different component + "esphome/components/api/api_server.cpp", + "esphome/components/api/custom_api_device.h", + ] + + with patch("helpers.get_changed_components") as mock_components: + mock_components.return_value = ["wifi"] # Only wifi, not wifi_info + + result = filter_changed(all_files) + + # Should only include files from wifi component, not wifi_info + expected = ["esphome/components/wifi/wifi_component.cpp"] + assert result == expected + + +def test_filter_changed_prints_output( + monkeypatch: MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """Test that appropriate messages are printed.""" + monkeypatch.setenv("GITHUB_ACTIONS", "true") + + all_files = ["esphome/components/wifi/wifi_component.cpp"] + + with patch("helpers.get_changed_components") as mock_components: + mock_components.return_value = ["wifi"] + + filter_changed(all_files) + + # Check that output was produced (not checking exact messages) + captured = capsys.readouterr() + assert len(captured.out) > 0 + + +@pytest.mark.parametrize( + ("files", "expected_empty"), + [ + ([], True), + (["file.cpp"], False), + ], + ids=["empty_files", "non_empty_files"], +) +def test_filter_changed_empty_file_handling( + monkeypatch: MonkeyPatch, files: list[str], expected_empty: bool +) -> None: + """Test handling of empty file lists.""" + monkeypatch.setenv("GITHUB_ACTIONS", "true") + + with patch("helpers.get_changed_components") as mock_components: + mock_components.return_value = ["wifi"] + + result = filter_changed(files) + + # Both cases should be empty: + # - Empty files list -> empty result + # - file.cpp doesn't match esphome/components/wifi/* pattern -> filtered out + assert len(result) == 0 + + +def test_filter_changed_ci_full_scan() -> None: + """Test _filter_changed_ci when core C++/header files changed (full scan).""" + all_files = ["esphome/components/wifi/wifi.cpp", "esphome/core/helpers.cpp"] + + with patch("helpers.get_changed_components", return_value=None): + result = _filter_changed_ci(all_files) + + # Should return all files for full scan + assert result == all_files + + +def test_filter_changed_ci_no_components_changed() -> None: + """Test _filter_changed_ci when no components changed.""" + all_files = ["esphome/components/wifi/wifi.cpp", "script/clang-tidy", "README.md"] + + with ( + patch("helpers.get_changed_components", return_value=[]), + patch("helpers.changed_files", return_value=["script/clang-tidy", "README.md"]), + ): + result = _filter_changed_ci(all_files) + + # Should only include non-component files that changed + assert set(result) == {"script/clang-tidy", "README.md"} + + +def test_filter_changed_ci_specific_components() -> None: + """Test _filter_changed_ci with specific components changed.""" + all_files = [ + "esphome/components/wifi/wifi.cpp", + "esphome/components/wifi/wifi.h", + "esphome/components/api/api.cpp", + "esphome/components/mqtt/mqtt.cpp", + ] + + with patch("helpers.get_changed_components", return_value=["wifi", "api"]): + result = _filter_changed_ci(all_files) + + # Should include all files from wifi and api components + expected = [ + "esphome/components/wifi/wifi.cpp", + "esphome/components/wifi/wifi.h", + "esphome/components/api/api.cpp", + ] + assert set(result) == set(expected) + + +def test_filter_changed_local() -> None: + """Test _filter_changed_local filters based on git changes.""" + all_files = [ + "esphome/components/wifi/wifi.cpp", + "esphome/components/api/api.cpp", + "esphome/core/helpers.cpp", + ] + + with patch("helpers.changed_files") as mock_changed: + mock_changed.return_value = [ + "esphome/components/wifi/wifi.cpp", + "esphome/core/helpers.cpp", + ] + + result = _filter_changed_local(all_files) + + # Should only include files that actually changed + expected = ["esphome/components/wifi/wifi.cpp", "esphome/core/helpers.cpp"] + assert set(result) == set(expected) + + +def test_build_all_include_with_git(tmp_path: Path) -> None: + """Test build_all_include using git ls-files.""" + # Mock git output + git_output = "esphome/core/component.h\nesphome/components/wifi/wifi.h\nesphome/components/api/api.h\n" + + mock_proc = Mock() + mock_proc.returncode = 0 + mock_proc.stdout = git_output + + with ( + patch("subprocess.run", return_value=mock_proc), + patch("helpers.temp_header_file", str(tmp_path / "all-include.cpp")), + ): + build_all_include() + + # Check the generated file + include_file = tmp_path / "all-include.cpp" + assert include_file.exists() + + content = include_file.read_text() + expected_lines = [ + '#include "esphome/components/api/api.h"', + '#include "esphome/components/wifi/wifi.h"', + '#include "esphome/core/component.h"', + "", # Empty line at end + ] + assert content == "\n".join(expected_lines) + + +def test_build_all_include_empty_output(tmp_path: Path) -> None: + """Test build_all_include with empty git output.""" + # Mock git returning empty output + mock_proc = Mock() + mock_proc.returncode = 0 + mock_proc.stdout = "" + + with ( + patch("subprocess.run", return_value=mock_proc), + patch("helpers.temp_header_file", str(tmp_path / "all-include.cpp")), + ): + build_all_include() + + # Check the generated file + include_file = tmp_path / "all-include.cpp" + assert include_file.exists() + + content = include_file.read_text() + # When git output is empty, the list comprehension filters out empty strings, + # then we append "" to get [""], which joins to just "" + assert content == "" + + +def test_build_all_include_creates_directory(tmp_path: Path) -> None: + """Test that build_all_include creates the temp directory if needed.""" + # Use a subdirectory that doesn't exist + temp_file = tmp_path / "subdir" / "all-include.cpp" + + mock_proc = Mock() + mock_proc.returncode = 0 + mock_proc.stdout = "esphome/core/test.h\n" + + with ( + patch("subprocess.run", return_value=mock_proc), + patch("helpers.temp_header_file", str(temp_file)), + ): + build_all_include() + + # Check that directory was created + assert temp_file.parent.exists() + assert temp_file.exists() + + +def test_print_file_list_empty(capsys: pytest.CaptureFixture[str]) -> None: + """Test printing an empty file list.""" + print_file_list([], "Test Files:") + captured = capsys.readouterr() + + assert "Test Files:" in captured.out + assert "No files to check!" in captured.out + + +def test_print_file_list_small(capsys: pytest.CaptureFixture[str]) -> None: + """Test printing a small list of files (less than max_files).""" + files = ["file1.cpp", "file2.cpp", "file3.cpp"] + print_file_list(files, "Test Files:", max_files=20) + captured = capsys.readouterr() + + assert "Test Files:" in captured.out + assert " file1.cpp" in captured.out + assert " file2.cpp" in captured.out + assert " file3.cpp" in captured.out + assert "... and" not in captured.out + + +def test_print_file_list_exact_max_files(capsys: pytest.CaptureFixture[str]) -> None: + """Test printing exactly max_files number of files.""" + files = [f"file{i}.cpp" for i in range(20)] + print_file_list(files, "Test Files:", max_files=20) + captured = capsys.readouterr() + + # All files should be shown + for i in range(20): + assert f" file{i}.cpp" in captured.out + assert "... and" not in captured.out + + +def test_print_file_list_large(capsys: pytest.CaptureFixture[str]) -> None: + """Test printing a large list of files (more than max_files).""" + files = [f"file{i:03d}.cpp" for i in range(50)] + print_file_list(files, "Test Files:", max_files=20) + captured = capsys.readouterr() + + assert "Test Files:" in captured.out + # First 10 files should be shown (sorted) + for i in range(10): + assert f" file{i:03d}.cpp" in captured.out + # Files 10-49 should not be shown + assert " file010.cpp" not in captured.out + assert " file049.cpp" not in captured.out + # Should show count of remaining files + assert "... and 40 more files" in captured.out + + +def test_print_file_list_unsorted(capsys: pytest.CaptureFixture[str]) -> None: + """Test that files are sorted before printing.""" + files = ["z_file.cpp", "a_file.cpp", "m_file.cpp"] + print_file_list(files, "Test Files:", max_files=20) + captured = capsys.readouterr() + + lines = captured.out.strip().split("\n") + # Check order in output + assert lines[1] == " a_file.cpp" + assert lines[2] == " m_file.cpp" + assert lines[3] == " z_file.cpp" + + +def test_print_file_list_custom_max_files(capsys: pytest.CaptureFixture[str]) -> None: + """Test with custom max_files parameter.""" + files = [f"file{i}.cpp" for i in range(15)] + print_file_list(files, "Test Files:", max_files=10) + captured = capsys.readouterr() + + # Should truncate after 10 files + assert "... and 5 more files" in captured.out + + +def test_print_file_list_default_title(capsys: pytest.CaptureFixture[str]) -> None: + """Test with default title.""" + print_file_list(["test.cpp"]) + captured = capsys.readouterr() + + assert "Files:" in captured.out + assert " test.cpp" in captured.out + + +@pytest.mark.parametrize( + ("component_configs", "initial_components", "expected_components"), + [ + # No dependencies + ( + {"sensor": ([], [])}, # (dependencies, auto_load) + {"sensor"}, + {"sensor"}, + ), + # Simple dependencies + ( + { + "sensor": (["esp32"], []), + "esp32": ([], []), + }, + {"sensor"}, + {"sensor", "esp32"}, + ), + # Auto-load components + ( + { + "light": ([], ["output", "power_supply"]), + "output": ([], []), + "power_supply": ([], []), + }, + {"light"}, + {"light", "output", "power_supply"}, + ), + # Transitive dependencies + ( + { + "comp_a": (["comp_b"], []), + "comp_b": (["comp_c"], []), + "comp_c": ([], []), + }, + {"comp_a"}, + {"comp_a", "comp_b", "comp_c"}, + ), + # Dependencies with dots (sensor.base) + ( + { + "my_comp": (["sensor.base", "binary_sensor.base"], []), + "sensor": ([], []), + "binary_sensor": ([], []), + }, + {"my_comp"}, + {"my_comp", "sensor", "binary_sensor"}, + ), + # Circular dependencies (should not cause infinite loop) + ( + { + "comp_a": (["comp_b"], []), + "comp_b": (["comp_a"], []), + }, + {"comp_a"}, + {"comp_a", "comp_b"}, + ), + ], +) +def test_get_all_dependencies( + component_configs: dict[str, tuple[list[str], list[str]]], + initial_components: set[str], + expected_components: set[str], +) -> None: + """Test dependency resolution for components.""" + with patch("esphome.loader.get_component") as mock_get_component: + + def get_component_side_effect(name: str): + if name in component_configs: + deps, auto_load = component_configs[name] + comp = Mock() + comp.dependencies = deps + comp.auto_load = auto_load + return comp + return None + + mock_get_component.side_effect = get_component_side_effect + + result = helpers.get_all_dependencies(initial_components) + + assert result == expected_components + + +def test_get_all_dependencies_handles_missing_components() -> None: + """Test handling of components that can't be loaded.""" + with patch("esphome.loader.get_component") as mock_get_component: + # First component exists, its dependency doesn't + comp = Mock() + comp.dependencies = ["missing_comp"] + comp.auto_load = [] + + mock_get_component.side_effect = ( + lambda name: comp if name == "existing" else None + ) + + result = helpers.get_all_dependencies({"existing", "nonexistent"}) + + # Should still include all components, even if some can't be loaded + assert result == {"existing", "nonexistent", "missing_comp"} + + +def test_get_all_dependencies_empty_set() -> None: + """Test with empty initial component set.""" + result = helpers.get_all_dependencies(set()) + assert result == set() + + +def test_get_components_from_integration_fixtures() -> None: + """Test extraction of components from fixture YAML files.""" + yaml_content = { + "sensor": [{"platform": "template", "name": "test"}], + "binary_sensor": [{"platform": "gpio", "pin": 5}], + "esphome": {"name": "test"}, + "api": {}, + } + expected_components = { + "sensor", + "binary_sensor", + "esphome", + "api", + "template", + "gpio", + } + + mock_yaml_file = Mock() + + with ( + patch("pathlib.Path.glob") as mock_glob, + patch("esphome.yaml_util.load_yaml", return_value=yaml_content), + ): + mock_glob.return_value = [mock_yaml_file] + + components = helpers.get_components_from_integration_fixtures() + + assert components == expected_components + + +@pytest.mark.parametrize( + "output,expected", + [ + ("wifi\napi\nsensor\n", ["wifi", "api", "sensor"]), + ("wifi\n", ["wifi"]), + ("", []), + (" \n \n", []), + ("\n\n", []), + (" wifi \n api \n", ["wifi", "api"]), + ("wifi\n\napi\n\nsensor", ["wifi", "api", "sensor"]), + ], +) +def test_parse_list_components_output(output: str, expected: list[str]) -> None: + """Test parse_list_components_output function.""" + result = helpers.parse_list_components_output(output) + assert result == expected 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 0dcdd84507..c639ad94b2 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -8,9 +8,19 @@ from typing import Any import pytest from esphome.config_validation import Invalid -from esphome.const import CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ICON, CONF_NAME +from esphome.const import ( + CONF_DEVICE_ID, + CONF_DISABLED_BY_DEFAULT, + CONF_ICON, + CONF_INTERNAL, + CONF_NAME, +) from esphome.core import CORE, ID, entity_helpers -from esphome.core.entity_helpers import get_base_entity_object_id, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + get_base_entity_object_id, + setup_entity, +) from esphome.cpp_generator import MockObj from esphome.helpers import sanitize, snake_case @@ -493,11 +503,6 @@ async def test_setup_entity_disabled_by_default( def test_entity_duplicate_validator() -> None: """Test the entity_duplicate_validator function.""" - from esphome.core.entity_helpers import entity_duplicate_validator - - # Reset CORE unique_ids for clean test - CORE.unique_ids.clear() - # Create validator for sensor platform validator = entity_duplicate_validator("sensor") @@ -505,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"} @@ -523,11 +528,6 @@ def test_entity_duplicate_validator() -> None: def test_entity_duplicate_validator_with_devices() -> None: """Test entity_duplicate_validator with devices.""" - from esphome.core.entity_helpers import entity_duplicate_validator - - # Reset CORE unique_ids for clean test - CORE.unique_ids.clear() - # Create validator for sensor platform validator = entity_duplicate_validator("sensor") @@ -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 ) @@ -605,3 +593,37 @@ def test_entity_different_platforms_yaml_validation( ) # This should succeed assert result is not None + + +def test_entity_duplicate_validator_internal_entities() -> None: + """Test that internal entities are excluded from duplicate name validation.""" + # Create validator for sensor platform + validator = entity_duplicate_validator("sensor") + + # First entity should pass + config1 = {CONF_NAME: "Temperature"} + validated1 = validator(config1) + assert validated1 == config1 + # 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 + + # 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 + + # Non-internal entity with same name should fail + config4 = {CONF_NAME: "Temperature"} + with pytest.raises( + Invalid, match=r"Duplicate sensor entity with name 'Temperature' found" + ): + validator(config4) diff --git a/tests/unit_tests/fixtures/substitutions/.gitignore b/tests/unit_tests/fixtures/substitutions/.gitignore index 0b15cdb2b7..897c9da86c 100644 --- a/tests/unit_tests/fixtures/substitutions/.gitignore +++ b/tests/unit_tests/fixtures/substitutions/.gitignore @@ -1 +1 @@ -*.received.yaml \ No newline at end of file +*.received.yaml diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml index c031399c37..f5d2f8aa20 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml @@ -17,3 +17,5 @@ test_list: - ${undefined_var} - $undefined_var - ${ undefined_var } + - key1: 1 + key2: 2 diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml index 88a4ffb991..5717433c7e 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml @@ -19,3 +19,5 @@ test_list: - ${undefined_var} - $undefined_var - ${ undefined_var } + - key${var1}: 1 + key${var2}: 2 diff --git a/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml b/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml index c8f7d9976c..dad3be8aa7 100644 --- a/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml @@ -6,6 +6,7 @@ package_result: root file - Double substitution also works; the value of var7 is 79, where A is a package var + - key79: Key should substitute to key79 local_results: - The value of B is 5 - 'You will see, however, that diff --git a/tests/unit_tests/fixtures/substitutions/closures_package.yaml b/tests/unit_tests/fixtures/substitutions/closures_package.yaml index e87908814d..4a0be24b93 100644 --- a/tests/unit_tests/fixtures/substitutions/closures_package.yaml +++ b/tests/unit_tests/fixtures/substitutions/closures_package.yaml @@ -1,3 +1,4 @@ package_result: - The value of A*B is ${A * B}, where A is a package var and B is a substitution in the root file - Double substitution also works; the value of var7 is ${var$A}, where A is a package var + - key${var7}: Key should substitute to key79 diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 4f2a6453b4..f7dda9fb95 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -473,6 +473,61 @@ class TestLibrary: assert actual == expected + @pytest.mark.parametrize( + "target, other, result, exception", + ( + (core.Library("libfoo", None), core.Library("libfoo", None), True, None), + ( + core.Library("libfoo", "1.2.3"), + core.Library("libfoo", "1.2.3"), + True, # target is unchanged + None, + ), + ( + core.Library("libfoo", None), + core.Library("libfoo", "1.2.3"), + False, # Use version from other + None, + ), + ( + core.Library("libfoo", "1.2.3"), + core.Library("libfoo", "1.2.4"), + False, + ValueError, # Version mismatch + ), + ( + core.Library("libfoo", "1.2.3"), + core.Library("libbar", "1.2.3"), + False, + ValueError, # Name mismatch + ), + ( + core.Library( + "libfoo", "1.2.4", "https://github.com/esphome/ESPAsyncWebServer" + ), + core.Library("libfoo", "1.2.3"), + True, # target is unchanged due to having a repository + None, + ), + ( + core.Library("libfoo", "1.2.3"), + core.Library( + "libfoo", "1.2.4", "https://github.com/esphome/ESPAsyncWebServer" + ), + False, # use other due to having a repository + None, + ), + ), + ) + def test_reconcile(self, target, other, result, exception): + if exception is not None: + with pytest.raises(exception): + target.reconcile_with(other) + else: + expected = target if result else other + actual = target.reconcile_with(other) + assert actual == expected + class TestEsphomeCore: @pytest.fixture diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index b377499d29..3208923116 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -47,9 +47,8 @@ def dict_diff(a, b, path=""): elif len(b) > len(a): for i in range(min_len, len(b)): diffs.append(f"{path}[{i}] only in expected: {b[i]!r}") - else: - if a != b: - diffs.append(f"\t{path}: actual={a!r} expected={b!r}") + elif a != b: + diffs.append(f"\t{path}: actual={a!r} expected={b!r}") return diffs