diff --git a/.ai/instructions.md b/.ai/instructions.md new file mode 100644 index 0000000000..6c002f9617 --- /dev/null +++ b/.ai/instructions.md @@ -0,0 +1,224 @@ +# 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`. +* **Static Analysis & Development:** + * `esphome/core/defines.h`: A comprehensive header file containing all `#define` directives that can be added by components using `cg.add_define()` in Python. This file is used exclusively for development, static analysis tools, and CI testing - it is not used during runtime compilation. When developing components that add new defines, they must be added to this file to ensure proper IDE support and static analysis coverage. The file includes feature flags, build configurations, and platform-specific defines that help static analyzers understand the complete codebase without needing to compile for specific platforms. + +## 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..30cf982649 --- /dev/null +++ b/.clang-tidy.hash @@ -0,0 +1 @@ +6af8b429b94191fe8e239fcb3b73f7982d0266cb5b05ffbc81edaeac1bc8c273 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..9fb80e6a9d 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -22,7 +22,7 @@ runs: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.2.3 + uses: actions/cache/restore@v4.2.4 with: path: venv # yamllint disable-line rule:line-length @@ -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..63f059eb6d --- /dev/null +++ b/.github/workflows/auto-label-pr.yml @@ -0,0 +1,652 @@ +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 + COMPONENT_LABEL_THRESHOLD: 10 + +jobs: + label: + runs-on: ubuntu-latest + if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' + steps: + - name: Checkout + uses: actions/checkout@v5.0.0 + + - 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'); + + // Constants + const SMALL_PR_THRESHOLD = parseInt('${{ env.SMALL_PR_THRESHOLD }}'); + const MAX_LABELS = parseInt('${{ env.MAX_LABELS }}'); + const TOO_BIG_THRESHOLD = parseInt('${{ env.TOO_BIG_THRESHOLD }}'); + const COMPONENT_LABEL_THRESHOLD = parseInt('${{ env.COMPONENT_LABEL_THRESHOLD }}'); + const BOT_COMMENT_MARKER = ''; + const CODEOWNERS_MARKER = ''; + const TOO_BIG_MARKER = ''; + + const MANAGED_LABELS = [ + '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', + 'needs-codeowners', + 'too-big', + 'labeller-recheck', + 'bugfix', + 'new-feature', + 'breaking-change', + 'code-quality' + ]; + + const DOCS_PR_PATTERNS = [ + /https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/, + /esphome\/esphome-docs#\d+/ + ]; + + // Global state + const { owner, repo } = context.repo; + const pr_number = context.issue.number; + + // Get current labels and PR data + const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: pr_number + }); + const currentLabels = currentLabelsData.map(label => label.name); + const managedLabels = currentLabels.filter(label => + label.startsWith('component: ') || MANAGED_LABELS.includes(label) + ); + + // Check for mega-PR early - if present, skip most automatic labeling + const isMegaPR = currentLabels.includes('mega-pr'); + + // Get all PR files with automatic pagination + const prFiles = await github.paginate( + github.rest.pulls.listFiles, + { + owner, + repo, + pull_number: pr_number + } + ); + + // Calculate data from PR files + const changedFiles = prFiles.map(file => file.filename); + const totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); + + console.log('Current labels:', currentLabels.join(', ')); + console.log('Changed files:', changedFiles.length); + console.log('Total changes:', totalChanges); + if (isMegaPR) { + console.log('Mega-PR detected - applying limited labeling logic'); + } + + // Fetch API data + async function fetchApiData() { + try { + const response = await fetch('https://data.esphome.io/components.json'); + const componentsData = await response.json(); + return { + targetPlatforms: componentsData.target_platforms || [], + platformComponents: componentsData.platform_components || [] + }; + } catch (error) { + console.log('Failed to fetch components data from API:', error.message); + return { targetPlatforms: [], platformComponents: [] }; + } + } + + // Strategy: Merge branch detection + async function detectMergeBranch() { + const labels = new Set(); + const baseRef = context.payload.pull_request.base.ref; + + if (baseRef === 'release') { + labels.add('merging-to-release'); + } else if (baseRef === 'beta') { + labels.add('merging-to-beta'); + } + + return labels; + } + + // Strategy: Component and platform labeling + async function detectComponentPlatforms(apiData) { + const labels = new Set(); + const componentRegex = /^esphome\/components\/([^\/]+)\//; + const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`); + + for (const file of changedFiles) { + const componentMatch = file.match(componentRegex); + if (componentMatch) { + labels.add(`component: ${componentMatch[1]}`); + } + + const platformMatch = file.match(targetPlatformRegex); + if (platformMatch) { + labels.add(`platform: ${platformMatch[1]}`); + } + } + + return labels; + } + + // Strategy: New component detection + async function detectNewComponents() { + const labels = new Set(); + const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); + + for (const file of addedFiles) { + const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/); + if (componentMatch) { + try { + const content = fs.readFileSync(file, 'utf8'); + if (content.includes('IS_TARGET_PLATFORM = True')) { + labels.add('new-target-platform'); + } + } catch (error) { + console.log(`Failed to read content of ${file}:`, error.message); + } + labels.add('new-component'); + } + } + + return labels; + } + + // Strategy: New platform detection + async function detectNewPlatforms(apiData) { + const labels = new Set(); + const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename); + + for (const file of addedFiles) { + const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/); + if (platformFileMatch) { + const [, component, platform] = platformFileMatch; + if (apiData.platformComponents.includes(platform)) { + labels.add('new-platform'); + } + } + + const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/); + if (platformDirMatch) { + const [, component, platform] = platformDirMatch; + if (apiData.platformComponents.includes(platform)) { + labels.add('new-platform'); + } + } + } + + return labels; + } + + // Strategy: Core files detection + async function detectCoreChanges() { + const labels = new Set(); + const coreFiles = changedFiles.filter(file => + file.startsWith('esphome/core/') || + (file.startsWith('esphome/') && file.split('/').length === 2) + ); + + if (coreFiles.length > 0) { + labels.add('core'); + } + + return labels; + } + + // Strategy: PR size detection + async function detectPRSize() { + const labels = new Set(); + const testChanges = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); + + const nonTestChanges = totalChanges - testChanges; + + if (totalChanges <= SMALL_PR_THRESHOLD) { + labels.add('small-pr'); + } + + // Don't add too-big if mega-pr label is already present + if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) { + labels.add('too-big'); + } + + return labels; + } + + // Strategy: Dashboard changes + async function detectDashboardChanges() { + const labels = new Set(); + const dashboardFiles = changedFiles.filter(file => + file.startsWith('esphome/dashboard/') || + file.startsWith('esphome/components/dashboard_import/') + ); + + if (dashboardFiles.length > 0) { + labels.add('dashboard'); + } + + return labels; + } + + // Strategy: GitHub Actions changes + async function detectGitHubActionsChanges() { + const labels = new Set(); + const githubActionsFiles = changedFiles.filter(file => + file.startsWith('.github/workflows/') + ); + + if (githubActionsFiles.length > 0) { + labels.add('github-actions'); + } + + return labels; + } + + // Strategy: Code owner detection + async function detectCodeOwner() { + const labels = new Set(); + + try { + 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; + + const codeownersLines = codeownersContent.split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + + const codeownersRegexes = codeownersLines.map(line => { + const parts = line.split(/\s+/); + const pattern = parts[0]; + const owners = parts.slice(1); + + let regex; + if (pattern.endsWith('*')) { + const dir = pattern.slice(0, -1); + regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`); + } else if (pattern.includes('*')) { + // First escape all regex special chars except *, then replace * with .* + const regexPattern = pattern + .replace(/[.+?^${}()|[\]\\]/g, '\\$&') + .replace(/\*/g, '.*'); + regex = new RegExp(`^${regexPattern}$`); + } else { + regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); + } + + return { regex, owners }; + }); + + for (const file of changedFiles) { + for (const { regex, owners } of codeownersRegexes) { + if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) { + labels.add('by-code-owner'); + return labels; + } + } + } + } catch (error) { + console.log('Failed to read or parse CODEOWNERS file:', error.message); + } + + return labels; + } + + // Strategy: Test detection + async function detectTests() { + const labels = new Set(); + const testFiles = changedFiles.filter(file => file.startsWith('tests/')); + + if (testFiles.length > 0) { + labels.add('has-tests'); + } + + return labels; + } + + // Strategy: PR Template Checkbox detection + async function detectPRTemplateCheckboxes() { + const labels = new Set(); + const prBody = context.payload.pull_request.body || ''; + + console.log('Checking PR template checkboxes...'); + + // Check for checked checkboxes in the "Types of changes" section + const checkboxPatterns = [ + { pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' }, + { pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' }, + { pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' }, + { pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' } + ]; + + for (const { pattern, label } of checkboxPatterns) { + if (pattern.test(prBody)) { + console.log(`Found checked checkbox for: ${label}`); + labels.add(label); + } + } + + return labels; + } + + // Strategy: Requirements detection + async function detectRequirements(allLabels) { + const labels = new Set(); + + // Check for missing tests + if ((allLabels.has('new-component') || allLabels.has('new-platform')) && !allLabels.has('has-tests')) { + labels.add('needs-tests'); + } + + // Check for missing docs + if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) { + const prBody = context.payload.pull_request.body || ''; + const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody)); + + if (!hasDocsLink) { + labels.add('needs-docs'); + } + } + + // Check for missing CODEOWNERS + if (allLabels.has('new-component')) { + const codeownersModified = prFiles.some(file => + file.filename === 'CODEOWNERS' && + (file.status === 'modified' || file.status === 'added') && + (file.additions || 0) > 0 + ); + + if (!codeownersModified) { + labels.add('needs-codeowners'); + } + } + + return labels; + } + + // Generate review messages + function generateReviewMessages(finalLabels) { + const messages = []; + const prAuthor = context.payload.pull_request.user.login; + + // Too big message + if (finalLabels.includes('too-big')) { + const testChanges = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); + const nonTestChanges = totalChanges - testChanges; + + const tooManyLabels = finalLabels.length > MAX_LABELS; + const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; + + let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`; + + if (tooManyLabels && tooManyChanges) { + message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`; + } else if (tooManyLabels) { + message += `This PR affects ${finalLabels.length} different components/areas.`; + } else { + message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`; + } + + message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`; + message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`; + + messages.push(message); + } + + // CODEOWNERS message + if (finalLabels.includes('needs-codeowners')) { + const message = `${CODEOWNERS_MARKER}\n### 👥 Code Ownership\n\n` + + `Hey there @${prAuthor},\n` + + `Thanks for submitting this pull request! Can you add yourself as a codeowner for this integration? ` + + `This way we can notify you if a bug report for this integration is reported.\n\n` + + `In \`__init__.py\` of the integration, please add:\n\n` + + `\`\`\`python\nCODEOWNERS = ["@${prAuthor}"]\n\`\`\`\n\n` + + `And run \`script/build_codeowners.py\``; + + messages.push(message); + } + + return messages; + } + + // Handle reviews + async function handleReviews(finalLabels) { + const reviewMessages = generateReviewMessages(finalLabels); + const hasReviewableLabels = finalLabels.some(label => + ['too-big', 'needs-codeowners'].includes(label) + ); + + const { data: reviews } = await github.rest.pulls.listReviews({ + owner, + repo, + pull_number: pr_number + }); + + const botReviews = reviews.filter(review => + review.user.type === 'Bot' && + review.state === 'CHANGES_REQUESTED' && + review.body && review.body.includes(BOT_COMMENT_MARKER) + ); + + if (hasReviewableLabels) { + const reviewBody = `${BOT_COMMENT_MARKER}\n\n${reviewMessages.join('\n\n---\n\n')}`; + + if (botReviews.length > 0) { + // Update existing review + await github.rest.pulls.updateReview({ + owner, + repo, + pull_number: pr_number, + review_id: botReviews[0].id, + body: reviewBody + }); + console.log('Updated existing bot review'); + } else { + // Create new review + await github.rest.pulls.createReview({ + owner, + repo, + pull_number: pr_number, + body: reviewBody, + event: 'REQUEST_CHANGES' + }); + console.log('Created new bot review'); + } + } else if (botReviews.length > 0) { + // Dismiss existing reviews + for (const review of botReviews) { + try { + await github.rest.pulls.dismissReview({ + owner, + repo, + pull_number: pr_number, + review_id: review.id, + message: 'Review dismissed: All requirements have been met' + }); + console.log(`Dismissed bot review ${review.id}`); + } catch (error) { + console.log(`Failed to dismiss review ${review.id}:`, error.message); + } + } + } + } + + // Main execution + const apiData = await fetchApiData(); + const baseRef = context.payload.pull_request.base.ref; + + // Early exit for non-dev branches + if (baseRef !== 'dev') { + const branchLabels = await detectMergeBranch(); + const finalLabels = Array.from(branchLabels); + + console.log('Computed labels (merge branch only):', finalLabels.join(', ')); + + // Apply labels + if (finalLabels.length > 0) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pr_number, + labels: finalLabels + }); + } + + // Remove old managed labels + const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label)); + for (const label of labelsToRemove) { + 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; + } + + // Run all strategies + const [ + branchLabels, + componentLabels, + newComponentLabels, + newPlatformLabels, + coreLabels, + sizeLabels, + dashboardLabels, + actionsLabels, + codeOwnerLabels, + testLabels, + checkboxLabels + ] = await Promise.all([ + detectMergeBranch(), + detectComponentPlatforms(apiData), + detectNewComponents(), + detectNewPlatforms(apiData), + detectCoreChanges(), + detectPRSize(), + detectDashboardChanges(), + detectGitHubActionsChanges(), + detectCodeOwner(), + detectTests(), + detectPRTemplateCheckboxes() + ]); + + // Combine all labels + const allLabels = new Set([ + ...branchLabels, + ...componentLabels, + ...newComponentLabels, + ...newPlatformLabels, + ...coreLabels, + ...sizeLabels, + ...dashboardLabels, + ...actionsLabels, + ...codeOwnerLabels, + ...testLabels, + ...checkboxLabels + ]); + + // Detect requirements based on all other labels + const requirementLabels = await detectRequirements(allLabels); + for (const label of requirementLabels) { + allLabels.add(label); + } + + let finalLabels = Array.from(allLabels); + + // For mega-PRs, exclude component labels if there are too many + if (isMegaPR) { + const componentLabels = finalLabels.filter(label => label.startsWith('component: ')); + if (componentLabels.length > COMPONENT_LABEL_THRESHOLD) { + finalLabels = finalLabels.filter(label => !label.startsWith('component: ')); + console.log(`Mega-PR detected - excluding ${componentLabels.length} component labels (threshold: ${COMPONENT_LABEL_THRESHOLD})`); + } + } + + // Handle too many labels (only for non-mega PRs) + const tooManyLabels = finalLabels.length > MAX_LABELS; + + if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) { + finalLabels = ['too-big']; + } + + console.log('Computed labels:', finalLabels.join(', ')); + + // Handle reviews + await handleReviews(finalLabels); + + // Apply 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 + 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-api-proto.yml b/.github/workflows/ci-api-proto.yml index f51bd84186..c7cc720323 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Set up Python uses: actions/setup-python@v5.6.0 with: diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml new file mode 100644 index 0000000000..c7da7f6672 --- /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@v5.0.0 + + - 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..61ecf8183b 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -43,11 +43,11 @@ jobs: - "docker" # - "lint" steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 - 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..e50f2aef40 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 @@ -36,10 +36,10 @@ jobs: cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - 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 @@ -47,7 +47,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: venv # yamllint disable-line rule:line-length @@ -58,59 +58,19 @@ 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 + uses: actions/checkout@v5.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -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 @@ -152,7 +91,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -173,7 +112,6 @@ jobs: fail-fast: false matrix: python-version: - - "3.10" - "3.11" - "3.12" - "3.13" @@ -189,21 +127,18 @@ 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 steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - 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,15 +159,62 @@ 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.4 + 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@v5.0.0 + 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 + uses: actions/checkout@v5.0.0 - name: Set up Python 3.13 id: python uses: actions/setup-python@v5.6.0 @@ -240,7 +222,7 @@ jobs: python-version: "3.13" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -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 @@ -328,13 +281,17 @@ jobs: pio_cache_key: tidyesp32-idf - id: clang-tidy name: Run script/clang-tidy for ZEPHYR - options: --environment nrf52-tidy --grep USE_ZEPHYR + options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52 pio_cache_key: tidy-zephyr ignore_errors: false steps: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 + with: + # Need history for HEAD~1 to work for checking changed files + fetch-depth: 2 + - name: Restore Python uses: ./.github/actions/restore-python with: @@ -343,17 +300,17 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 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 + uses: actions/cache/restore@v4.2.4 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: | @@ -440,7 +374,7 @@ jobs: sudo apt-get install libsdl2-dev - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -460,17 +394,17 @@ 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: - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - 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 @@ -496,7 +430,7 @@ jobs: sudo apt-get install libsdl2-dev - name: Check out code from GitHub - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -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@v5.0.0 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - uses: pre-commit/action@v3.0.1 + 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..ab3377365d --- /dev/null +++ b/.github/workflows/codeowner-review-request.yml @@ -0,0 +1,324 @@ +# 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 comments = await github.paginate( + 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.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/codeql.yml b/.github/workflows/codeql.yml index ddeb0a99d2..7a7c39aeec 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,7 +54,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5.0.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/external-component-bot.yml b/.github/workflows/external-component-bot.yml new file mode 100644 index 0000000000..29103e8eee --- /dev/null +++ b/.github/workflows/external-component-bot.yml @@ -0,0 +1,157 @@ +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 = ""; + const legacyCommentMarker = ""; + 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.paginate( + github.rest.issues.listComments, + { + owner: owner, + repo: repo, + issue_number: prNumber, + per_page: 100, + } + ); + + const sorted = comments.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); + + const botComment = sorted.find(comment => + ( + comment.body.includes(commentMarker) || + comment.body.includes(legacyCommentMarker) + ) && comment.user.type === "Bot" + ); + + 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..3639d346f5 --- /dev/null +++ b/.github/workflows/issue-codeowner-notify.yml @@ -0,0 +1,163 @@ +# 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}`); + + // Hidden marker to identify bot comments from this workflow + const BOT_COMMENT_MARKER = ''; + + // 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 comments = await github.paginate( + 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.body.includes(BOT_COMMENT_MARKER) && + comment.body.includes(`component: ${componentName}`) + ); + + // 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 = `${BOT_COMMENT_MARKER}\n👋 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..9af67aa310 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: branch_build: ${{ steps.tag.outputs.branch_build }} deploy_env: ${{ steps.tag.outputs.deploy_env }} steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 - name: Get tag id: tag # yamllint disable rule:line-length @@ -60,7 +60,7 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 - name: Set up Python uses: actions/setup-python@v5.6.0 with: @@ -92,22 +92,22 @@ jobs: os: "ubuntu-24.04-arm" steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 - 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 - name: Log in to docker hub - uses: docker/login-action@v3.4.0 + uses: docker/login-action@v3.5.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry - uses: docker/login-action@v3.4.0 + uses: docker/login-action@v3.5.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -168,10 +168,10 @@ jobs: - ghcr - dockerhub steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v5.0.0 - name: Download digests - uses: actions/download-artifact@v4.3.0 + uses: actions/download-artifact@v5.0.0 with: pattern: digests-* path: /tmp/digests @@ -182,13 +182,13 @@ jobs: - name: Log in to docker hub if: matrix.registry == 'dockerhub' - uses: docker/login-action@v3.4.0 + uses: docker/login-action@v3.5.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry if: matrix.registry == 'ghcr' - uses: docker/login-action@v3.4.0 + uses: docker/login-action@v3.5.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index a38825fc45..cc03ed3e3f 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -13,10 +13,10 @@ jobs: if: github.repository == 'esphome/esphome' steps: - name: Checkout - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 - name: Checkout Home Assistant - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5.0.0 with: repository: home-assistant/core path: lib/home-assistant 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 83ea2dc823..1830e7881c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,15 +4,14 @@ ci: autoupdate_commit_msg: 'pre-commit: autoupdate' - autoupdate_schedule: weekly - autofix_prs: false + 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, yamllint] + skip: [pylint, clang-tidy-hash] repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.2 + rev: v0.12.8 hooks: # Run the linter. - id: ruff @@ -28,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: @@ -56,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 9b4681fcf2..a1a74e9c99 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 @@ -39,11 +40,11 @@ esphome/components/analog_threshold/* @ianchi esphome/components/animation/* @syndlex esphome/components/anova/* @buxtronix esphome/components/apds9306/* @aodrenah -esphome/components/api/* @OttoWinter +esphome/components/api/* @esphome/core esphome/components/as5600/* @ammmze esphome/components/as5600/sensor/* @ammmze esphome/components/as7341/* @mrgnr -esphome/components/async_tcp/* @OttoWinter +esphome/components/async_tcp/* @esphome/core esphome/components/at581x/* @X-Ryl669 esphome/components/atc_mithermometer/* @ahpohl esphome/components/atm90e26/* @danieltwagner @@ -68,7 +69,7 @@ esphome/components/bl0939/* @ziceva esphome/components/bl0940/* @tobias- esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/ble_client/* @buxtronix @clydebarrow -esphome/components/bluetooth_proxy/* @jesserockz +esphome/components/bluetooth_proxy/* @bdraco @jesserockz esphome/components/bme280_base/* @esphome/core esphome/components/bme280_spi/* @apbodrov esphome/components/bme680_bsec/* @trvrnrth @@ -90,7 +91,7 @@ esphome/components/bytebuffer/* @clydebarrow esphome/components/camera/* @DT-art1 @bdraco esphome/components/canbus/* @danielschramm @mvturnho esphome/components/cap1188/* @mreditor97 -esphome/components/captive_portal/* @OttoWinter +esphome/components/captive_portal/* @esphome/core esphome/components/ccs811/* @habbie esphome/components/cd74hc4067/* @asoehlke esphome/components/ch422g/* @clydebarrow @jesterret @@ -117,7 +118,7 @@ esphome/components/dallas_temp/* @ssieb esphome/components/daly_bms/* @s1lvi0 esphome/components/dashboard_import/* @esphome/core esphome/components/datetime/* @jesserockz @rfdarter -esphome/components/debug/* @OttoWinter +esphome/components/debug/* @esphome/core esphome/components/delonghi/* @grob6000 esphome/components/dfplayer/* @glmnet esphome/components/dfrobot_sen0395/* @niklasweber @@ -143,9 +144,10 @@ esphome/components/es8156/* @kbx81 esphome/components/es8311/* @kahrendt @kroimon esphome/components/es8388/* @P4uLT esphome/components/esp32/* @esphome/core -esphome/components/esp32_ble/* @Rapsssito @jesserockz -esphome/components/esp32_ble_client/* @jesserockz +esphome/components/esp32_ble/* @Rapsssito @bdraco @jesserockz +esphome/components/esp32_ble_client/* @bdraco @jesserockz esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz +esphome/components/esp32_ble_tracker/* @bdraco esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron esphome/components/esp32_hosted/* @swoboda1337 @@ -154,6 +156,7 @@ esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp8266/* @esphome/core esphome/components/esp_ldo/* @clydebarrow +esphome/components/espnow/* @jesserockz esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/event/* @nohat esphome/components/event_emitter/* @Rapsssito @@ -235,7 +238,7 @@ esphome/components/integration/* @OttoWinter esphome/components/internal_temperature/* @Mat931 esphome/components/interval/* @esphome/core esphome/components/jsn_sr04t/* @Mafus1 -esphome/components/json/* @OttoWinter +esphome/components/json/* @esphome/core esphome/components/kamstrup_kmp/* @cfeenstra1024 esphome/components/key_collector/* @ssieb esphome/components/key_provider/* @ssieb @@ -243,8 +246,10 @@ esphome/components/kuntze/* @ssieb esphome/components/lc709203f/* @ilikecake esphome/components/lcd_menu/* @numo68 esphome/components/ld2410/* @regevbr @sebcaps +esphome/components/ld2412/* @Rihan9 esphome/components/ld2420/* @descipher esphome/components/ld2450/* @hareeshmu +esphome/components/ld24xx/* @kbx81 esphome/components/ledc/* @OttoWinter esphome/components/libretiny/* @kuba2k2 esphome/components/libretiny_pwm/* @kuba2k2 @@ -292,6 +297,7 @@ esphome/components/microphone/* @jesserockz @kahrendt esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov +esphome/components/mipi_dsi/* @clydebarrow esphome/components/mipi_spi/* @clydebarrow esphome/components/mitsubishi/* @RubyBailey esphome/components/mixer/speaker/* @kahrendt @@ -324,6 +330,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw esphome/components/nfc/* @jesserockz @kbx81 esphome/components/noblex/* @AGalfra esphome/components/npi19/* @bakerkj +esphome/components/nrf52/* @tomaszduda23 esphome/components/number/* @esphome/core esphome/components/one_wire/* @ssieb esphome/components/online_image/* @clydebarrow @guillempages @@ -378,6 +385,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 @@ -461,13 +469,13 @@ esphome/components/template/event/* @nohat esphome/components/template/fan/* @ssieb esphome/components/text/* @mauritskorse esphome/components/thermostat/* @kbx81 -esphome/components/time/* @OttoWinter +esphome/components/time/* @esphome/core esphome/components/tlc5947/* @rnauber esphome/components/tlc5971/* @IJIJI esphome/components/tm1621/* @Philippe12 esphome/components/tm1637/* @glmnet esphome/components/tm1638/* @skykingjwc -esphome/components/tm1651/* @freekode +esphome/components/tm1651/* @mrtoy-me esphome/components/tmp102/* @timsavage esphome/components/tmp1075/* @sybrenstuvel esphome/components/tmp117/* @Azimath @@ -505,7 +513,7 @@ esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/watchdog/* @oarcher esphome/components/waveshare_epaper/* @clydebarrow esphome/components/web_server/ota/* @esphome/core -esphome/components/web_server_base/* @OttoWinter +esphome/components/web_server_base/* @esphome/core esphome/components/web_server_idf/* @dentra esphome/components/weikai/* @DrCoolZic esphome/components/weikai_i2c/* @DrCoolZic @@ -535,5 +543,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 63f48f1238..6566b56e4b 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.5 +PROJECT_NUMBER = 2025.8.0b1 # 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/docker/build.py b/docker/build.py index 921adac7ab..4d093cf88d 100755 --- a/docker/build.py +++ b/docker/build.py @@ -90,7 +90,7 @@ def main(): def run_command(*cmd, ignore_error: bool = False): print(f"$ {shlex.join(list(cmd))}") if not args.dry_run: - rc = subprocess.call(list(cmd)) + rc = subprocess.call(list(cmd), close_fds=False) if rc != 0 and not ignore_error: print("Command failed") sys.exit(1) diff --git a/esphome/__main__.py b/esphome/__main__.py index d8a79c018a..7cc8296e7e 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -2,12 +2,14 @@ import argparse from datetime import datetime import functools +import getpass import importlib import logging import os import re import sys import time +from typing import Protocol import argcomplete @@ -34,6 +36,7 @@ from esphome.const import ( CONF_PORT, CONF_SUBSTITUTIONS, CONF_TOPIC, + ENV_NOGITIGNORE, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -42,6 +45,7 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError, coroutine from esphome.helpers import get_bool_env, indent, is_ip_address from esphome.log import AnsiFore, color, setup_log +from esphome.types import ConfigType from esphome.util import ( get_serial_ports, list_yaml_files, @@ -53,6 +57,23 @@ from esphome.util import ( _LOGGER = logging.getLogger(__name__) +class ArgsProtocol(Protocol): + device: list[str] | None + reset: bool + username: str | None + password: str | None + client_id: str | None + topic: str | None + file: str | None + no_logs: bool + only_generate: bool + show_secrets: bool + dashboard: bool + configuration: str + name: str + upload_speed: str | None + + def choose_prompt(options, purpose: str = None): if not options: raise EsphomeError( @@ -86,30 +107,54 @@ def choose_prompt(options, purpose: str = None): def choose_upload_log_host( - default, check_default, show_ota, show_mqtt, show_api, purpose: str = None -): - options = [] - for port in get_serial_ports(): - options.append((f"{port.path} ({port.description})", port.path)) - if default == "SERIAL": - return choose_prompt(options, purpose=purpose) + default: list[str] | str | None, + check_default: str | None, + show_ota: bool, + show_mqtt: bool, + show_api: bool, + purpose: str | None = None, +) -> list[str]: + # Convert to list for uniform handling + defaults = [default] if isinstance(default, str) else default or [] + + # If devices specified, resolve them + if defaults: + resolved: list[str] = [] + for device in defaults: + if device == "SERIAL": + serial_ports = get_serial_ports() + if not serial_ports: + _LOGGER.warning("No serial ports found, skipping SERIAL device") + continue + options = [ + (f"{port.path} ({port.description})", port.path) + for port in serial_ports + ] + resolved.append(choose_prompt(options, purpose=purpose)) + elif device == "OTA": + if (show_ota and "ota" in CORE.config) or ( + show_api and "api" in CORE.config + ): + resolved.append(CORE.address) + elif show_mqtt and has_mqtt_logging(): + resolved.append("MQTT") + else: + resolved.append(device) + return resolved + + # No devices specified, show interactive chooser + options = [ + (f"{port.path} ({port.description})", port.path) for port in get_serial_ports() + ] if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config): options.append((f"Over The Air ({CORE.address})", CORE.address)) - if default == "OTA": - return CORE.address - if ( - show_mqtt - and (mqtt_config := CORE.config.get(CONF_MQTT)) - and mqtt_logging_enabled(mqtt_config) - ): + if show_mqtt and has_mqtt_logging(): + mqtt_config = CORE.config[CONF_MQTT] options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT")) - if default == "OTA": - return "MQTT" - if default is not None: - return default + if check_default is not None and check_default in [opt[1] for opt in options]: - return check_default - return choose_prompt(options, purpose=purpose) + return [check_default] + return [choose_prompt(options, purpose=purpose)] def mqtt_logging_enabled(mqtt_config): @@ -118,12 +163,17 @@ def mqtt_logging_enabled(mqtt_config): return False if CONF_TOPIC not in log_topic: return False - if log_topic.get(CONF_LEVEL, None) == "NONE": - return False - return True + return log_topic.get(CONF_LEVEL, None) != "NONE" -def get_port_type(port): +def has_mqtt_logging() -> bool: + """Check if MQTT logging is available.""" + return (mqtt_config := CORE.config.get(CONF_MQTT)) and mqtt_logging_enabled( + mqtt_config + ) + + +def get_port_type(port: str) -> str: if port.startswith("/") or port.startswith("COM"): return "SERIAL" if port == "MQTT": @@ -131,7 +181,7 @@ def get_port_type(port): return "NETWORK" -def run_miniterm(config, port, args): +def run_miniterm(config: ConfigType, port: str, args) -> int: from aioesphomeapi import LogParser import serial @@ -208,12 +258,15 @@ def wrap_to_code(name, comp): return wrapped -def write_cpp(config): +def write_cpp(config: ConfigType) -> int: + if not get_bool_env(ENV_NOGITIGNORE): + writer.write_gitignore() + generate_cpp_contents(config) return write_cpp_file() -def generate_cpp_contents(config): +def generate_cpp_contents(config: ConfigType) -> None: _LOGGER.info("Generating C++ source...") for name, component, conf in iter_component_configs(CORE.config): @@ -224,15 +277,18 @@ def generate_cpp_contents(config): CORE.flush_tasks() -def write_cpp_file(): - writer.write_platformio_project() - +def write_cpp_file() -> int: code_s = indent(CORE.cpp_main_section) writer.write_cpp(code_s) + + from esphome.build_gen import platformio + + platformio.write_project() + return 0 -def compile_program(args, config): +def compile_program(args: ArgsProtocol, config: ConfigType) -> int: from esphome import platformio_api _LOGGER.info("Compiling app...") @@ -243,7 +299,9 @@ def compile_program(args, config): return 0 if idedata is not None else 1 -def upload_using_esptool(config, port, file, speed): +def upload_using_esptool( + config: ConfigType, port: str, file: str, speed: int +) -> str | int: from esphome import platformio_api first_baudrate = speed or config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get( @@ -271,20 +329,20 @@ def upload_using_esptool(config, port, file, speed): def run_esptool(baud_rate): cmd = [ - "esptool.py", + "esptool", "--before", - "default_reset", + "default-reset", "--after", - "hard_reset", + "hard-reset", "--baud", str(baud_rate), "--port", port, "--chip", mcu, - "write_flash", + "write-flash", "-z", - "--flash_size", + "--flash-size", "detect", ] for img in flash_images: @@ -308,7 +366,7 @@ def upload_using_esptool(config, port, file, speed): return run_esptool(115200) -def upload_using_platformio(config, port): +def upload_using_platformio(config: ConfigType, port: str): from esphome import platformio_api upload_args = ["-t", "upload", "-t", "nobuild"] @@ -317,7 +375,7 @@ def upload_using_platformio(config, port): return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) -def check_permissions(port): +def check_permissions(port: str): if os.name == "posix" and get_port_type(port) == "SERIAL": # Check if we can open selected serial port if not os.access(port, os.F_OK): @@ -330,12 +388,12 @@ def check_permissions(port): raise EsphomeError( "You do not have read or write permission on the selected serial port. " "To resolve this issue, you can add your user to the dialout group " - f"by running the following command: sudo usermod -a -G dialout {os.getlogin()}. " + f"by running the following command: sudo usermod -a -G dialout {getpass.getuser()}. " "You will need to log out & back in or reboot to activate the new group access." ) -def upload_program(config, args, host): +def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | str: try: module = importlib.import_module("esphome.components." + CORE.target_platform) if getattr(module, "upload_program")(config, args, host): @@ -350,7 +408,7 @@ def upload_program(config, args, host): return upload_using_esptool(config, host, file, args.upload_speed) if CORE.target_platform in (PLATFORM_RP2040): - return upload_using_platformio(config, args.device) + return upload_using_platformio(config, host) if CORE.is_libretiny: return upload_using_platformio(config, host) @@ -373,9 +431,12 @@ def upload_program(config, args, host): remote_port = int(ota_conf[CONF_PORT]) password = ota_conf.get(CONF_PASSWORD, "") + # Check if we should use MQTT for address resolution + # This happens when no device was specified, or the current host is "MQTT"/"OTA" + devices: list[str] = args.device or [] if ( CONF_MQTT in config # pylint: disable=too-many-boolean-expressions - and (not args.device or args.device in ("MQTT", "OTA")) + and (not devices or host in ("MQTT", "OTA")) and ( ((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address)) or get_port_type(host) == "MQTT" @@ -393,23 +454,28 @@ def upload_program(config, args, host): return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) -def show_logs(config, args, port): +def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None: if "logger" not in config: raise EsphomeError("Logger is not configured!") + + port = devices[0] + if get_port_type(port) == "SERIAL": check_permissions(port) return run_miniterm(config, port, args) if get_port_type(port) == "NETWORK" and "api" in config: + addresses_to_use = devices if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config: from esphome import mqtt - port = mqtt.get_esphome_device_ip( + mqtt_address = mqtt.get_esphome_device_ip( config, args.username, args.password, args.client_id )[0] + addresses_to_use = [mqtt_address] from esphome.components.api.client import run_logs - return run_logs(config, port) + return run_logs(config, addresses_to_use) if get_port_type(port) == "MQTT" and "mqtt" in config: from esphome import mqtt @@ -420,7 +486,7 @@ def show_logs(config, args, port): raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)") -def clean_mqtt(config, args): +def clean_mqtt(config: ConfigType, args: ArgsProtocol) -> int | None: from esphome import mqtt return mqtt.clear_topic( @@ -428,13 +494,13 @@ def clean_mqtt(config, args): ) -def command_wizard(args): +def command_wizard(args: ArgsProtocol) -> int | None: from esphome import wizard return wizard.wizard(args.configuration) -def command_config(args, config): +def command_config(args: ArgsProtocol, config: ConfigType) -> int | None: if not CORE.verbose: config = strip_default_ids(config) output = yaml_util.dump(config, args.show_secrets) @@ -449,7 +515,7 @@ def command_config(args, config): return 0 -def command_vscode(args): +def command_vscode(args: ArgsProtocol) -> int | None: from esphome import vscode logging.disable(logging.INFO) @@ -457,7 +523,7 @@ def command_vscode(args): vscode.read_config(args) -def command_compile(args, config): +def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: exit_code = write_cpp(config) if exit_code != 0: return exit_code @@ -471,8 +537,9 @@ def command_compile(args, config): return 0 -def command_upload(args, config): - port = choose_upload_log_host( +def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None: + # Get devices, resolving special identifiers like OTA + devices = choose_upload_log_host( default=args.device, check_default=None, show_ota=True, @@ -480,14 +547,22 @@ def command_upload(args, config): show_api=False, purpose="uploading", ) - exit_code = upload_program(config, args, port) - if exit_code != 0: - return exit_code - _LOGGER.info("Successfully uploaded program.") - return 0 + + # Try each device until one succeeds + exit_code = 1 + for device in devices: + _LOGGER.info("Uploading to %s", device) + exit_code = upload_program(config, args, device) + if exit_code == 0: + _LOGGER.info("Successfully uploaded program.") + return 0 + if len(devices) > 1: + _LOGGER.warning("Failed to upload to %s", device) + + return exit_code -def command_discover(args, config): +def command_discover(args: ArgsProtocol, config: ConfigType) -> int | None: if "mqtt" in config: from esphome import mqtt @@ -496,8 +571,9 @@ def command_discover(args, config): raise EsphomeError("No discover method configured (mqtt)") -def command_logs(args, config): - port = choose_upload_log_host( +def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None: + # Get devices, resolving special identifiers like OTA + devices = choose_upload_log_host( default=args.device, check_default=None, show_ota=False, @@ -505,10 +581,10 @@ def command_logs(args, config): show_api=True, purpose="logging", ) - return show_logs(config, args, port) + return show_logs(config, args, devices) -def command_run(args, config): +def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: exit_code = write_cpp(config) if exit_code != 0: return exit_code @@ -525,7 +601,8 @@ def command_run(args, config): program_path = idedata.raw["prog_path"] return run_external_process(program_path) - port = choose_upload_log_host( + # Get devices, resolving special identifiers like OTA + devices = choose_upload_log_host( default=args.device, check_default=None, show_ota=True, @@ -533,39 +610,53 @@ def command_run(args, config): show_api=True, purpose="uploading", ) - exit_code = upload_program(config, args, port) - if exit_code != 0: + + # Try each device for upload until one succeeds + successful_device: str | None = None + for device in devices: + _LOGGER.info("Uploading to %s", device) + exit_code = upload_program(config, args, device) + if exit_code == 0: + _LOGGER.info("Successfully uploaded program.") + successful_device = device + break + if len(devices) > 1: + _LOGGER.warning("Failed to upload to %s", device) + + if successful_device is None: return exit_code - _LOGGER.info("Successfully uploaded program.") + if args.no_logs: return 0 - port = choose_upload_log_host( - default=args.device, - check_default=port, + + # For logs, prefer the device we successfully uploaded to + devices = choose_upload_log_host( + default=successful_device, + check_default=successful_device, show_ota=False, show_mqtt=True, show_api=True, purpose="logging", ) - return show_logs(config, args, port) + return show_logs(config, args, devices) -def command_clean_mqtt(args, config): +def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None: return clean_mqtt(config, args) -def command_mqtt_fingerprint(args, config): +def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None: from esphome import mqtt return mqtt.get_fingerprint(config) -def command_version(args): +def command_version(args: ArgsProtocol) -> int | None: safe_print(f"Version: {const.__version__}") return 0 -def command_clean(args, config): +def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None: try: writer.clean_build() except OSError as err: @@ -575,13 +666,13 @@ def command_clean(args, config): return 0 -def command_dashboard(args): +def command_dashboard(args: ArgsProtocol) -> int | None: from esphome.dashboard import dashboard return dashboard.start_dashboard(args) -def command_update_all(args): +def command_update_all(args: ArgsProtocol) -> int | None: import click success = {} @@ -628,7 +719,7 @@ def command_update_all(args): return failed -def command_idedata(args, config): +def command_idedata(args: ArgsProtocol, config: ConfigType) -> int: import json from esphome import platformio_api @@ -644,7 +735,7 @@ def command_idedata(args, config): return 0 -def command_rename(args, config): +def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: for c in args.name: if c not in ALLOWED_NAME_CHARS: print( @@ -761,6 +852,12 @@ POST_CONFIG_ACTIONS = { "discover": command_discover, } +SIMPLE_CONFIG_ACTIONS = [ + "clean", + "clean-mqtt", + "config", +] + def parse_args(argv): options_parser = argparse.ArgumentParser(add_help=False) @@ -848,7 +945,8 @@ def parse_args(argv): ) parser_upload.add_argument( "--device", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", + action="append", + help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", ) parser_upload.add_argument( "--upload_speed", @@ -870,7 +968,8 @@ def parse_args(argv): ) parser_logs.add_argument( "--device", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", + action="append", + help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", ) parser_logs.add_argument( "--reset", @@ -899,7 +998,8 @@ def parse_args(argv): ) parser_run.add_argument( "--device", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", + action="append", + help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", ) parser_run.add_argument( "--upload_speed", @@ -1026,6 +1126,13 @@ def parse_args(argv): arguments = argv[1:] argcomplete.autocomplete(parser) + + if len(arguments) > 0 and arguments[0] in SIMPLE_CONFIG_ACTIONS: + args, unknown_args = parser.parse_known_args(arguments) + if unknown_args: + _LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args) + return args + return parser.parse_args(arguments) diff --git a/esphome/automation.py b/esphome/automation.py index 34159561c2..99d4362845 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -391,8 +391,7 @@ async def build_action(full_config, template_arg, args): ) action_id = full_config[CONF_TYPE_ID] builder = registry_entry.coroutine_fun - ret = await builder(config, action_id, template_arg, args) - return ret + return await builder(config, action_id, template_arg, args) async def build_action_list(config, templ, arg_type): @@ -409,8 +408,7 @@ async def build_condition(full_config, template_arg, args): ) action_id = full_config[CONF_TYPE_ID] builder = registry_entry.coroutine_fun - ret = await builder(config, action_id, template_arg, args) - return ret + return await builder(config, action_id, template_arg, args) async def build_condition_list(config, templ, args): 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/a4988/a4988.cpp b/esphome/components/a4988/a4988.cpp index 72b3835cfd..b9efb4ea44 100644 --- a/esphome/components/a4988/a4988.cpp +++ b/esphome/components/a4988/a4988.cpp @@ -7,7 +7,6 @@ namespace a4988 { static const char *const TAG = "a4988.stepper"; void A4988::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (this->sleep_pin_ != nullptr) { this->sleep_pin_->setup(); this->sleep_pin_->digital_write(false); diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index c3cb159aed..b8717ac5f1 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -7,8 +7,6 @@ namespace absolute_humidity { static const char *const TAG = "absolute_humidity.sensor"; void AbsoluteHumidityComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); - ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str()); this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); }); if (this->temperature_sensor_->has_state()) { diff --git a/esphome/components/absolute_humidity/sensor.py b/esphome/components/absolute_humidity/sensor.py index 62a2c8ab7c..caaa546e25 100644 --- a/esphome/components/absolute_humidity/sensor.py +++ b/esphome/components/absolute_humidity/sensor.py @@ -5,7 +5,7 @@ from esphome.const import ( CONF_EQUATION, CONF_HUMIDITY, CONF_TEMPERATURE, - ICON_WATER, + DEVICE_CLASS_ABSOLUTE_HUMIDITY, STATE_CLASS_MEASUREMENT, UNIT_GRAMS_PER_CUBIC_METER, ) @@ -27,8 +27,8 @@ EQUATION = { CONFIG_SCHEMA = ( sensor.sensor_schema( unit_of_measurement=UNIT_GRAMS_PER_CUBIC_METER, - icon=ICON_WATER, accuracy_decimals=2, + device_class=DEVICE_CLASS_ABSOLUTE_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ) .extend( diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 10b7df8638..f260e13242 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -1,10 +1,11 @@ from esphome import pins import esphome.codegen as cg -from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C2, VARIANT_ESP32C3, + VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2, VARIANT_ESP32S2, @@ -51,82 +52,103 @@ 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, + }, + VARIANT_ESP32P4: { + 16: adc_channel_t.ADC_CHANNEL_0, + 17: adc_channel_t.ADC_CHANNEL_1, + 18: adc_channel_t.ADC_CHANNEL_2, + 19: adc_channel_t.ADC_CHANNEL_3, + 20: adc_channel_t.ADC_CHANNEL_4, + 21: adc_channel_t.ADC_CHANNEL_5, + 22: adc_channel_t.ADC_CHANNEL_6, + 23: adc_channel_t.ADC_CHANNEL_7, }, } @@ -135,54 +157,64 @@ 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, + }, + VARIANT_ESP32P4: { + 49: adc_channel_t.ADC_CHANNEL_0, + 50: adc_channel_t.ADC_CHANNEL_1, + 51: adc_channel_t.ADC_CHANNEL_2, + 52: adc_channel_t.ADC_CHANNEL_3, + 53: adc_channel_t.ADC_CHANNEL_4, + 54: adc_channel_t.ADC_CHANNEL_5, }, } @@ -235,6 +267,11 @@ def validate_adc_pin(value): {CONF_ANALOG: True, CONF_INPUT: True}, internal=True )(value) + if CORE.is_nrf52: + return pins.gpio_pin_schema( + {CONF_ANALOG: True, CONF_INPUT: True}, internal=True + )(value) + raise NotImplementedError @@ -251,5 +288,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, + "adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 28dfd2262c..526dd57fd5 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -3,12 +3,19 @@ #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 + +#ifdef USE_ZEPHYR +#include +#endif namespace esphome { namespace adc { @@ -35,50 +42,91 @@ enum class SamplingMode : uint8_t { const LogString *sampling_mode_to_str(SamplingMode mode); -class Aggregator { +template class Aggregator { public: Aggregator(SamplingMode mode); - void add_sample(uint32_t value); - uint32_t aggregate(); + void add_sample(T value); + T aggregate(); protected: - uint32_t aggr_{0}; - uint32_t samples_{0}; + T aggr_{0}; + uint8_t samples_{0}; SamplingMode mode_{SamplingMode::AVG}; }; 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; + +#ifdef USE_ZEPHYR + /// Set the ADC channel to be used by the ADC sensor. + /// @param channel Pointer to an adc_dt_spec structure representing the ADC channel. + void set_adc_channel(const adc_dt_spec *channel) { this->channel_ = channel; } +#endif + /// 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 +138,32 @@ 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 +#ifdef USE_ZEPHYR + const struct adc_dt_spec *channel_ = nullptr; +#endif }; } // namespace adc diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp index 797ab75045..748c8634b7 100644 --- a/esphome/components/adc/adc_sensor_common.cpp +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -18,15 +18,15 @@ const LogString *sampling_mode_to_str(SamplingMode mode) { return LOG_STR("unknown"); } -Aggregator::Aggregator(SamplingMode mode) { +template Aggregator::Aggregator(SamplingMode mode) { this->mode_ = mode; // set to max uint if mode is "min" if (mode == SamplingMode::MIN) { - this->aggr_ = UINT32_MAX; + this->aggr_ = std::numeric_limits::max(); } } -void Aggregator::add_sample(uint32_t value) { +template void Aggregator::add_sample(T value) { this->samples_ += 1; switch (this->mode_) { @@ -47,7 +47,7 @@ void Aggregator::add_sample(uint32_t value) { } } -uint32_t Aggregator::aggregate() { +template T Aggregator::aggregate() { if (this->mode_ == SamplingMode::AVG) { if (this->samples_ == 0) { return this->aggr_; @@ -59,6 +59,12 @@ uint32_t Aggregator::aggregate() { return this->aggr_; } +#ifdef USE_ZEPHYR +template class Aggregator; +#else +template class Aggregator; +#endif + void ADCSensor::update() { float value_v = this->sample(); ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v); diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index ed1f3329ab..87d4ddd35f 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -8,145 +8,313 @@ 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() { + // 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; + +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 + // 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 || USE_ESP32_VARIANT_ESP32P4 + 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 || USE_ESP32_VARIANT_ESP32P4 + 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 || USE_ESP32_VARIANT_ESP32P4 + 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 || USE_ESP32_VARIANT_ESP32P4 + 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 || USE_ESP32_VARIANT_ESP32P4 + 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 +323,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..be14b252d4 100644 --- a/esphome/components/adc/adc_sensor_esp8266.cpp +++ b/esphome/components/adc/adc_sensor_esp8266.cpp @@ -17,7 +17,6 @@ namespace adc { static const char *const TAG = "adc.esp8266"; void ADCSensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); #ifndef USE_ADC_SENSOR_VCC this->pin_->setup(); #endif @@ -38,7 +37,7 @@ void ADCSensor::dump_config() { } float ADCSensor::sample() { - auto aggr = Aggregator(this->sampling_mode_); + auto aggr = Aggregator(this->sampling_mode_); for (uint8_t sample = 0; sample < this->sample_count_; sample++) { uint32_t raw = 0; @@ -56,8 +55,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/adc_sensor_libretiny.cpp b/esphome/components/adc/adc_sensor_libretiny.cpp index f7c7e669ec..0b1393c2e7 100644 --- a/esphome/components/adc/adc_sensor_libretiny.cpp +++ b/esphome/components/adc/adc_sensor_libretiny.cpp @@ -9,7 +9,6 @@ namespace adc { static const char *const TAG = "adc.libretiny"; void ADCSensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); #ifndef USE_ADC_SENSOR_VCC this->pin_->setup(); #endif // !USE_ADC_SENSOR_VCC @@ -31,7 +30,7 @@ void ADCSensor::dump_config() { float ADCSensor::sample() { uint32_t raw = 0; - auto aggr = Aggregator(this->sampling_mode_); + auto aggr = Aggregator(this->sampling_mode_); if (this->output_raw_) { for (uint8_t sample = 0; sample < this->sample_count_; sample++) { diff --git a/esphome/components/adc/adc_sensor_rp2040.cpp b/esphome/components/adc/adc_sensor_rp2040.cpp index 91d331270b..8496e0f41e 100644 --- a/esphome/components/adc/adc_sensor_rp2040.cpp +++ b/esphome/components/adc/adc_sensor_rp2040.cpp @@ -14,7 +14,6 @@ namespace adc { static const char *const TAG = "adc.rp2040"; void ADCSensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); static bool initialized = false; if (!initialized) { adc_init(); @@ -42,7 +41,7 @@ void ADCSensor::dump_config() { float ADCSensor::sample() { uint32_t raw = 0; - auto aggr = Aggregator(this->sampling_mode_); + auto aggr = Aggregator(this->sampling_mode_); if (this->is_temperature_) { adc_set_temp_sensor_enabled(true); diff --git a/esphome/components/adc/adc_sensor_zephyr.cpp b/esphome/components/adc/adc_sensor_zephyr.cpp new file mode 100644 index 0000000000..2fb9d4b0e5 --- /dev/null +++ b/esphome/components/adc/adc_sensor_zephyr.cpp @@ -0,0 +1,207 @@ + +#include "adc_sensor.h" +#ifdef USE_ZEPHYR +#include "esphome/core/log.h" + +#include "hal/nrf_saadc.h" + +namespace esphome { +namespace adc { + +static const char *const TAG = "adc.zephyr"; + +void ADCSensor::setup() { + if (!adc_is_ready_dt(this->channel_)) { + ESP_LOGE(TAG, "ADC controller device %s not ready", this->channel_->dev->name); + return; + } + + auto err = adc_channel_setup_dt(this->channel_); + if (err < 0) { + ESP_LOGE(TAG, "Could not setup channel %s (%d)", this->channel_->dev->name, err); + return; + } +} + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE +static const LogString *gain_to_str(enum adc_gain gain) { + switch (gain) { + case ADC_GAIN_1_6: + return LOG_STR("1/6"); + case ADC_GAIN_1_5: + return LOG_STR("1/5"); + case ADC_GAIN_1_4: + return LOG_STR("1/4"); + case ADC_GAIN_1_3: + return LOG_STR("1/3"); + case ADC_GAIN_2_5: + return LOG_STR("2/5"); + case ADC_GAIN_1_2: + return LOG_STR("1/2"); + case ADC_GAIN_2_3: + return LOG_STR("2/3"); + case ADC_GAIN_4_5: + return LOG_STR("4/5"); + case ADC_GAIN_1: + return LOG_STR("1"); + case ADC_GAIN_2: + return LOG_STR("2"); + case ADC_GAIN_3: + return LOG_STR("3"); + case ADC_GAIN_4: + return LOG_STR("4"); + case ADC_GAIN_6: + return LOG_STR("6"); + case ADC_GAIN_8: + return LOG_STR("8"); + case ADC_GAIN_12: + return LOG_STR("12"); + case ADC_GAIN_16: + return LOG_STR("16"); + case ADC_GAIN_24: + return LOG_STR("24"); + case ADC_GAIN_32: + return LOG_STR("32"); + case ADC_GAIN_64: + return LOG_STR("64"); + case ADC_GAIN_128: + return LOG_STR("128"); + } + return LOG_STR("undefined gain"); +} + +static const LogString *reference_to_str(enum adc_reference reference) { + switch (reference) { + case ADC_REF_VDD_1: + return LOG_STR("VDD"); + case ADC_REF_VDD_1_2: + return LOG_STR("VDD/2"); + case ADC_REF_VDD_1_3: + return LOG_STR("VDD/3"); + case ADC_REF_VDD_1_4: + return LOG_STR("VDD/4"); + case ADC_REF_INTERNAL: + return LOG_STR("INTERNAL"); + case ADC_REF_EXTERNAL0: + return LOG_STR("External, input 0"); + case ADC_REF_EXTERNAL1: + return LOG_STR("External, input 1"); + } + return LOG_STR("undefined reference"); +} + +static const LogString *input_to_str(uint8_t input) { + switch (input) { + case NRF_SAADC_INPUT_AIN0: + return LOG_STR("AIN0"); + case NRF_SAADC_INPUT_AIN1: + return LOG_STR("AIN1"); + case NRF_SAADC_INPUT_AIN2: + return LOG_STR("AIN2"); + case NRF_SAADC_INPUT_AIN3: + return LOG_STR("AIN3"); + case NRF_SAADC_INPUT_AIN4: + return LOG_STR("AIN4"); + case NRF_SAADC_INPUT_AIN5: + return LOG_STR("AIN5"); + case NRF_SAADC_INPUT_AIN6: + return LOG_STR("AIN6"); + case NRF_SAADC_INPUT_AIN7: + return LOG_STR("AIN7"); + case NRF_SAADC_INPUT_VDD: + return LOG_STR("VDD"); + case NRF_SAADC_INPUT_VDDHDIV5: + return LOG_STR("VDDHDIV5"); + } + return LOG_STR("undefined input"); +} +#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + +void ADCSensor::dump_config() { + LOG_SENSOR("", "ADC Sensor", this); + LOG_PIN(" Pin: ", this->pin_); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, + " Name: %s\n" + " Channel: %d\n" + " vref_mv: %d\n" + " Resolution %d\n" + " Oversampling %d", + this->channel_->dev->name, this->channel_->channel_id, this->channel_->vref_mv, this->channel_->resolution, + this->channel_->oversampling); + + ESP_LOGV(TAG, + " Gain: %s\n" + " reference: %s\n" + " acquisition_time: %d\n" + " differential %s", + LOG_STR_ARG(gain_to_str(this->channel_->channel_cfg.gain)), + LOG_STR_ARG(reference_to_str(this->channel_->channel_cfg.reference)), + this->channel_->channel_cfg.acquisition_time, YESNO(this->channel_->channel_cfg.differential)); + if (this->channel_->channel_cfg.differential) { + ESP_LOGV(TAG, + " Positive: %s\n" + " Negative: %s", + LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_positive)), + LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_negative))); + } else { + ESP_LOGV(TAG, " Positive: %s", LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_positive))); + } +#endif + + LOG_UPDATE_INTERVAL(this); +} + +float ADCSensor::sample() { + auto aggr = Aggregator(this->sampling_mode_); + int err; + for (uint8_t sample = 0; sample < this->sample_count_; sample++) { + int16_t buf = 0; + struct adc_sequence sequence = { + .buffer = &buf, + /* buffer size in bytes, not number of samples */ + .buffer_size = sizeof(buf), + }; + int32_t val_raw; + + err = adc_sequence_init_dt(this->channel_, &sequence); + if (err < 0) { + ESP_LOGE(TAG, "Could sequence init %s (%d)", this->channel_->dev->name, err); + return 0.0; + } + + err = adc_read(this->channel_->dev, &sequence); + if (err < 0) { + ESP_LOGE(TAG, "Could not read %s (%d)", this->channel_->dev->name, err); + return 0.0; + } + + val_raw = (int32_t) buf; + if (!this->channel_->channel_cfg.differential) { + // https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/0ed4d9ffc674ae407be7cacf5696a02f5e789861/cores/nRF5/wiring_analog_nRF52.c#L222 + if (val_raw < 0) { + val_raw = 0; + } + } + aggr.add_sample(val_raw); + } + + int32_t val_mv = aggr.aggregate(); + + if (this->output_raw_) { + return val_mv; + } + + err = adc_raw_to_millivolts_dt(this->channel_, &val_mv); + /* conversion to mV may not be supported, skip if not */ + if (err < 0) { + ESP_LOGE(TAG, "Value in mV not available %s (%d)", this->channel_->dev->name, err); + return 0.0; + } + + return val_mv / 1000.0f; +} + +} // namespace adc +} // namespace esphome +#endif diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 3309bd04c5..49970c5e3d 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -3,6 +3,12 @@ import logging import esphome.codegen as cg from esphome.components import sensor, voltage_sampler from esphome.components.esp32 import get_esp32_variant +from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC +from esphome.components.zephyr import ( + zephyr_add_overlay, + zephyr_add_prj_conf, + zephyr_add_user, +) import esphome.config_validation as cv from esphome.const import ( CONF_ATTENUATION, @@ -10,13 +16,12 @@ from esphome.const import ( CONF_NUMBER, CONF_PIN, CONF_RAW, - CONF_WIFI, DEVICE_CLASS_VOLTAGE, + PLATFORM_NRF52, STATE_CLASS_MEASUREMENT, UNIT_VOLT, ) from esphome.core import CORE -import esphome.final_validate as fv from . import ( ATTENUATION_MODES, @@ -24,6 +29,7 @@ from . import ( ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, SAMPLING_MODES, adc_ns, + adc_unit_t, validate_adc_pin, ) @@ -57,25 +63,14 @@ 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 ) +CONF_NRF_SAADC = "nrf_saadc" + +adc_dt_spec = cg.global_ns.class_("adc_dt_spec") + CONFIG_SCHEMA = cv.All( sensor.sensor_schema( ADCSensor, @@ -91,6 +86,7 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All( cv.only_on_esp32, _attenuation ), + cv.OnlyWith(CONF_NRF_SAADC, PLATFORM_NRF52): cv.declare_id(adc_dt_spec), cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255), cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode, } @@ -99,7 +95,7 @@ CONFIG_SCHEMA = cv.All( validate_config, ) -FINAL_VALIDATE_SCHEMA = final_validate_config +CONF_ADC_CHANNEL_ID = "adc_channel_id" async def to_code(config): @@ -111,7 +107,7 @@ async def to_code(config): cg.add_define("USE_ADC_SENSOR_VCC") elif config[CONF_PIN] == "TEMPERATURE": cg.add(var.set_is_temperature()) - else: + elif not CORE.is_nrf52 or config[CONF_PIN][CONF_NUMBER] not in EXTRA_ADC: pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) @@ -119,13 +115,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 +129,48 @@ 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)) + + elif CORE.is_nrf52: + CORE.data.setdefault(CONF_ADC_CHANNEL_ID, 0) + channel_id = CORE.data[CONF_ADC_CHANNEL_ID] + CORE.data[CONF_ADC_CHANNEL_ID] = channel_id + 1 + zephyr_add_prj_conf("ADC", True) + nrf_saadc = config[CONF_NRF_SAADC] + rhs = cg.RawExpression( + f"ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), {channel_id})" + ) + adc = cg.new_Pvariable(nrf_saadc, rhs) + cg.add(var.set_adc_channel(adc)) + gain = "ADC_GAIN_1_6" + pin_number = config[CONF_PIN][CONF_NUMBER] + if pin_number == "VDDHDIV5": + gain = "ADC_GAIN_1_2" + if isinstance(pin_number, int): + GPIO_TO_AIN = {v: k for k, v in AIN_TO_GPIO.items()} + pin_number = GPIO_TO_AIN[pin_number] + zephyr_add_user("io-channels", f"<&adc {channel_id}>") + zephyr_add_overlay( + f""" +&adc {{ + #address-cells = <1>; + #size-cells = <0>; + + channel@{channel_id} {{ + reg = <{channel_id}>; + zephyr,gain = "{gain}"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,input-positive = ; + zephyr,resolution = <14>; + zephyr,oversampling = <8>; + }}; +}}; +""" + ) diff --git a/esphome/components/adc128s102/adc128s102.cpp b/esphome/components/adc128s102/adc128s102.cpp index c8e8edb359..935dbde8ea 100644 --- a/esphome/components/adc128s102/adc128s102.cpp +++ b/esphome/components/adc128s102/adc128s102.cpp @@ -8,10 +8,7 @@ static const char *const TAG = "adc128s102"; float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; } -void ADC128S102::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->spi_setup(); -} +void ADC128S102::setup() { this->spi_setup(); } void ADC128S102::dump_config() { ESP_LOGCONFIG(TAG, "ADC128S102:"); diff --git a/esphome/components/ade7880/sensor.py b/esphome/components/ade7880/sensor.py index 3ef5e6bfff..39dbeb225f 100644 --- a/esphome/components/ade7880/sensor.py +++ b/esphome/components/ade7880/sensor.py @@ -36,6 +36,7 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.types import ConfigType DEPENDENCIES = ["i2c"] @@ -51,6 +52,20 @@ CONF_POWER_GAIN = "power_gain" CONF_NEUTRAL = "neutral" +# Tuple of power channel phases +POWER_PHASES = (CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C) + +# Tuple of sensor types that can be configured for power channels +POWER_SENSOR_TYPES = ( + CONF_CURRENT, + CONF_VOLTAGE, + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_POWER_FACTOR, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_REVERSE_ACTIVE_ENERGY, +) + NEUTRAL_CHANNEL_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(NeutralChannel), @@ -150,7 +165,64 @@ POWER_CHANNEL_SCHEMA = cv.Schema( } ) -CONFIG_SCHEMA = ( + +def prefix_sensor_name( + sensor_conf: ConfigType, + channel_name: str, + channel_config: ConfigType, + sensor_type: str, +) -> None: + """Helper to prefix sensor name with channel name. + + Args: + sensor_conf: The sensor configuration (dict or string) + channel_name: The channel name to prefix with + channel_config: The channel configuration to update + sensor_type: The sensor type key in the channel config + """ + if isinstance(sensor_conf, dict) and CONF_NAME in sensor_conf: + sensor_name = sensor_conf[CONF_NAME] + if sensor_name and not sensor_name.startswith(channel_name): + sensor_conf[CONF_NAME] = f"{channel_name} {sensor_name}" + elif isinstance(sensor_conf, str): + # Simple value case - convert to dict with prefixed name + channel_config[sensor_type] = {CONF_NAME: f"{channel_name} {sensor_conf}"} + + +def process_channel_sensors( + config: ConfigType, channel_key: str, sensor_types: tuple +) -> None: + """Process sensors for a channel and prefix their names. + + Args: + config: The main configuration + channel_key: The channel key (e.g., CONF_PHASE_A, CONF_NEUTRAL) + sensor_types: Tuple of sensor types to process for this channel + """ + if not (channel_config := config.get(channel_key)) or not ( + channel_name := channel_config.get(CONF_NAME) + ): + return + + for sensor_type in sensor_types: + if sensor_conf := channel_config.get(sensor_type): + prefix_sensor_name(sensor_conf, channel_name, channel_config, sensor_type) + + +def preprocess_channels(config: ConfigType) -> ConfigType: + """Preprocess channel configurations to add channel name prefix to sensor names.""" + # Process power channels + for channel in POWER_PHASES: + process_channel_sensors(config, channel, POWER_SENSOR_TYPES) + + # Process neutral channel + process_channel_sensors(config, CONF_NEUTRAL, (CONF_CURRENT,)) + + return config + + +CONFIG_SCHEMA = cv.All( + preprocess_channels, cv.Schema( { cv.GenerateID(): cv.declare_id(ADE7880), @@ -167,7 +239,7 @@ CONFIG_SCHEMA = ( } ) .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x38)) + .extend(i2c.i2c_device_schema(0x38)), ) @@ -188,15 +260,7 @@ async def neutral_channel(config): async def power_channel(config): var = cg.new_Pvariable(config[CONF_ID]) - for sensor_type in [ - CONF_CURRENT, - CONF_VOLTAGE, - CONF_ACTIVE_POWER, - CONF_APPARENT_POWER, - CONF_POWER_FACTOR, - CONF_FORWARD_ACTIVE_ENERGY, - CONF_REVERSE_ACTIVE_ENERGY, - ]: + for sensor_type in POWER_SENSOR_TYPES: if conf := config.get(sensor_type): sens = await sensor.new_sensor(conf) cg.add(getattr(var, f"set_{sensor_type}")(sens)) @@ -216,44 +280,6 @@ async def power_channel(config): return var -def final_validate(config): - for channel in [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]: - if channel := config.get(channel): - channel_name = channel.get(CONF_NAME) - - for sensor_type in [ - CONF_CURRENT, - CONF_VOLTAGE, - CONF_ACTIVE_POWER, - CONF_APPARENT_POWER, - CONF_POWER_FACTOR, - CONF_FORWARD_ACTIVE_ENERGY, - CONF_REVERSE_ACTIVE_ENERGY, - ]: - if conf := channel.get(sensor_type): - sensor_name = conf.get(CONF_NAME) - if ( - sensor_name - and channel_name - and not sensor_name.startswith(channel_name) - ): - conf[CONF_NAME] = f"{channel_name} {sensor_name}" - - if channel := config.get(CONF_NEUTRAL): - channel_name = channel.get(CONF_NAME) - if conf := channel.get(CONF_CURRENT): - sensor_name = conf.get(CONF_NAME) - if ( - sensor_name - and channel_name - and not sensor_name.startswith(channel_name) - ): - conf[CONF_NAME] = f"{channel_name} {sensor_name}" - - -FINAL_VALIDATE_SCHEMA = final_validate - - async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index 11a5663ed1..f4996cd3b1 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -10,7 +10,6 @@ static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; void ADS1115Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint16_t value; if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) { this->mark_failed(); diff --git a/esphome/components/ads1118/ads1118.cpp b/esphome/components/ads1118/ads1118.cpp index 1daa8fdfd4..f7db9f93dd 100644 --- a/esphome/components/ads1118/ads1118.cpp +++ b/esphome/components/ads1118/ads1118.cpp @@ -9,7 +9,6 @@ static const char *const TAG = "ads1118"; static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111; void ADS1118::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->config_ = 0; diff --git a/esphome/components/ags10/ags10.cpp b/esphome/components/ags10/ags10.cpp index 797a07afa5..9a29a979f3 100644 --- a/esphome/components/ags10/ags10.cpp +++ b/esphome/components/ags10/ags10.cpp @@ -24,8 +24,6 @@ static const uint16_t ZP_CURRENT = 0x0000; static const uint16_t ZP_DEFAULT = 0xFFFF; void AGS10Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - auto version = this->read_version_(); if (version) { ESP_LOGD(TAG, "AGS10 Sensor Version: 0x%02X", *version); @@ -45,8 +43,6 @@ void AGS10Component::setup() { } else { ESP_LOGE(TAG, "AGS10 Sensor Resistance: unknown"); } - - ESP_LOGD(TAG, "Sensor initialized"); } void AGS10Component::update() { diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 7f17e1c0d6..6202a27c42 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -38,8 +38,6 @@ static const uint8_t AHT10_STATUS_BUSY = 0x80; static const float AHT10_DIVISOR = 1048576.0f; // 2^20, used for temperature and humidity calculations void AHT10Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Reset failed"); } @@ -80,8 +78,6 @@ void AHT10Component::setup() { this->mark_failed(); return; } - - ESP_LOGV(TAG, "Initialization complete"); } void AHT10Component::restart_read_() { diff --git a/esphome/components/aic3204/aic3204.cpp b/esphome/components/aic3204/aic3204.cpp index a004fb42ce..e1acf32f83 100644 --- a/esphome/components/aic3204/aic3204.cpp +++ b/esphome/components/aic3204/aic3204.cpp @@ -17,8 +17,6 @@ static const char *const TAG = "aic3204"; } void AIC3204::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Set register page to 0 ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed"); // Initiate SW reset (PLL is powered off as part of reset) 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/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index 6d37d53a4c..058e061d1e 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -301,8 +301,7 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args): ) async def alarm_action_pending_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( @@ -310,8 +309,7 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args): ) async def alarm_action_trigger_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( @@ -319,8 +317,7 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args): ) async def alarm_action_chime_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( @@ -333,8 +330,7 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args): ) async def alarm_action_ready_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_condition( @@ -352,4 +348,3 @@ async def alarm_control_panel_is_armed_to_code( @coroutine_with_priority(100.0) async def to_code(config): cg.add_global(alarm_control_panel_ns.using) - cg.add_define("USE_ALARM_CONTROL_PANEL") diff --git a/esphome/components/am2315c/am2315c.cpp b/esphome/components/am2315c/am2315c.cpp index cea5263fd6..048c34d749 100644 --- a/esphome/components/am2315c/am2315c.cpp +++ b/esphome/components/am2315c/am2315c.cpp @@ -90,8 +90,6 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) { } void AM2315C::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // get status uint8_t status = 0; if (this->read(&status, 1) != i2c::ERROR_OK) { diff --git a/esphome/components/am2320/am2320.cpp b/esphome/components/am2320/am2320.cpp index 6400ecef4b..055be2aeee 100644 --- a/esphome/components/am2320/am2320.cpp +++ b/esphome/components/am2320/am2320.cpp @@ -34,7 +34,6 @@ void AM2320Component::update() { this->status_clear_warning(); } void AM2320Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t data[8]; data[0] = 0; data[1] = 4; diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index f73b8ef08f..c4ac7adb23 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -34,17 +34,20 @@ SetFrameAction = animation_ns.class_( "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) ) -CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend( - { - cv.Required(CONF_ID): cv.declare_id(Animation_), - cv.Optional(CONF_LOOP): cv.All( - { - cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, - cv.Optional(CONF_END_FRAME): cv.positive_int, - cv.Optional(CONF_REPEAT): cv.positive_int, - } - ), - }, +CONFIG_SCHEMA = cv.All( + espImage.IMAGE_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(Animation_), + cv.Optional(CONF_LOOP): cv.All( + { + cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, + cv.Optional(CONF_END_FRAME): cv.positive_int, + cv.Optional(CONF_REPEAT): cv.positive_int, + } + ), + }, + ), + espImage.validate_settings, ) diff --git a/esphome/components/apds9306/apds9306.cpp b/esphome/components/apds9306/apds9306.cpp index 9799f54d3d..fb3adde868 100644 --- a/esphome/components/apds9306/apds9306.cpp +++ b/esphome/components/apds9306/apds9306.cpp @@ -54,8 +54,6 @@ enum { // APDS9306 registers } void APDS9306::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - uint8_t id; if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register this->error_code_ = COMMUNICATION_FAILED; @@ -86,8 +84,6 @@ void APDS9306::setup() { // Set to active mode APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02); - - ESP_LOGCONFIG(TAG, "APDS9306 setup complete"); } void APDS9306::dump_config() { diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index b736e6b8b0..93038d3160 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -15,7 +15,6 @@ static const char *const TAG = "apds9960"; #define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value)); void APDS9960::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t id; if (!this->read_byte(0x92, &id)) { // ID register this->error_code_ = COMMUNICATION_FAILED; diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 5b302760b1..ba5f994e9a 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -29,7 +29,7 @@ from esphome.core import CORE, coroutine_with_priority DOMAIN = "api" DEPENDENCIES = ["network"] AUTO_LOAD = ["socket"] -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] api_ns = cg.esphome_ns.namespace("api") APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) @@ -53,6 +53,8 @@ SERVICE_ARG_NATIVE_TYPES = { CONF_ENCRYPTION = "encryption" CONF_BATCH_DELAY = "batch_delay" CONF_CUSTOM_SERVICES = "custom_services" +CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" +CONF_HOMEASSISTANT_STATES = "homeassistant_states" def validate_encryption_key(value): @@ -118,6 +120,8 @@ CONFIG_SCHEMA = cv.All( cv.Range(max=cv.TimePeriod(milliseconds=65535)), ), cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean, + cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean, + cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean, cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( single=True ), @@ -146,6 +150,12 @@ async def to_code(config): if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: cg.add_define("USE_API_SERVICES") + if config[CONF_HOMEASSISTANT_SERVICES]: + cg.add_define("USE_API_HOMEASSISTANT_SERVICES") + + if config[CONF_HOMEASSISTANT_STATES]: + cg.add_define("USE_API_HOMEASSISTANT_STATES") + if actions := config.get(CONF_ACTIONS, []): for conf in actions: template_args = [] @@ -235,6 +245,7 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( HOMEASSISTANT_ACTION_ACTION_SCHEMA, ) async def homeassistant_service_to_code(config, action_id, template_arg, args): + cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) templ = await cg.templatable(config[CONF_ACTION], args, None) @@ -278,6 +289,7 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema( HOMEASSISTANT_EVENT_ACTION_SCHEMA, ) async def homeassistant_event_to_code(config, action_id, template_arg, args): + cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, True) templ = await cg.templatable(config[CONF_EVENT], args, None) @@ -323,9 +335,10 @@ 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 - and user_services.cpp when no services are defined.""" - files_to_filter = [] + """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 @@ -341,4 +354,16 @@ def FILTER_SOURCE_FILES() -> list[str]: 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 861b3471d7..6b19f2026a 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", (fixed_array_size_define) = "ESPHOME_DEVICE_COUNT"]; + repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS", (fixed_array_size_define) = "ESPHOME_AREA_COUNT"]; // 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; @@ -380,15 +387,17 @@ message CoverCommandRequest { // 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; + uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } // ==================== FAN ==================== @@ -401,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; + repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"]; + 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; @@ -432,11 +443,12 @@ 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; @@ -448,8 +460,10 @@ message FanCommandRequest { 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; @@ -458,7 +472,7 @@ message FanCommandRequest { int32 speed_level = 11; bool has_preset_mode = 12; string preset_mode = 13; - uint32 device_id = 14; + uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; } // ==================== LIGHT ==================== @@ -484,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; + repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set"]; // 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; @@ -520,7 +538,7 @@ message LightStateResponse { float cold_white = 12; float warm_white = 13; string effect = 9; - uint32 device_id = 14; + uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; } message LightCommandRequest { option (id) = 32; @@ -556,7 +574,7 @@ message LightCommandRequest { uint32 flash_length = 17; bool has_effect = 18; string effect = 19; - uint32 device_id = 28; + uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"]; } // ==================== SENSOR ==================== @@ -567,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; @@ -582,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; @@ -608,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 ==================== @@ -621,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; @@ -639,7 +660,7 @@ message SwitchStateResponse { fixed32 key = 1; bool state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } message SwitchCommandRequest { option (id) = 33; @@ -650,7 +671,7 @@ message SwitchCommandRequest { fixed32 key = 1; bool state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== TEXT SENSOR ==================== @@ -663,13 +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; @@ -683,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 ==================== @@ -711,7 +732,6 @@ message SubscribeLogsResponse { LogLevel level = 1; bytes message = 3; - bool send_failed = 4; } // ==================== NOISE ENCRYPTION ==================== @@ -735,17 +755,19 @@ message NoiseEncryptionSetKeyResponse { message SubscribeHomeassistantServicesRequest { option (id) = 34; option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES"; } message HomeassistantServiceMap { string key = 1; - string value = 2; + string value = 2 [(no_zero_copy) = true]; } message HomeassistantServiceResponse { option (id) = 35; option (source) = SOURCE_SERVER; option (no_delay) = true; + option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES"; string service = 1; repeated HomeassistantServiceMap data = 2; @@ -761,11 +783,13 @@ message HomeassistantServiceResponse { message SubscribeHomeAssistantStatesRequest { option (id) = 38; option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_API_HOMEASSISTANT_STATES"; } message SubscribeHomeAssistantStateResponse { option (id) = 39; option (source) = SOURCE_SERVER; + option (ifdef) = "USE_API_HOMEASSISTANT_STATES"; string entity_id = 1; string attribute = 2; bool once = 3; @@ -775,6 +799,7 @@ message HomeAssistantStateResponse { option (id) = 40; option (source) = SOURCE_CLIENT; option (no_delay) = true; + option (ifdef) = "USE_API_HOMEASSISTANT_STATES"; string entity_id = 1; string state = 2; @@ -853,11 +878,11 @@ message ListEntitiesCameraResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id bool disabled_by_default = 5; - string icon = 6; + string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 7; - uint32 device_id = 8; + uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; } message CameraImageResponse { @@ -869,7 +894,7 @@ message CameraImageResponse { fixed32 key = 1; bytes data = 2; bool done = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message CameraImageRequest { option (id) = 45; @@ -937,32 +962,33 @@ 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; - repeated ClimateMode supported_modes = 7; + repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set"]; float visual_min_temperature = 8; float visual_max_temperature = 9; 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; - repeated string supported_custom_fan_modes = 15; - repeated ClimatePreset supported_presets = 16; - repeated string supported_custom_presets = 17; + repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set"]; + repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set"]; + repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"]; + repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set"]; + repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"]; bool 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; @@ -978,7 +1004,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; @@ -987,7 +1014,7 @@ message ClimateStateResponse { string custom_preset = 13; float current_humidity = 14; float target_humidity = 15; - uint32 device_id = 16; + uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"]; } message ClimateCommandRequest { option (id) = 48; @@ -1006,8 +1033,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; @@ -1020,7 +1049,7 @@ message ClimateCommandRequest { string custom_preset = 21; bool has_target_humidity = 22; float target_humidity = 23; - uint32 device_id = 24; + uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"]; } // ==================== NUMBER ==================== @@ -1038,9 +1067,9 @@ message ListEntitiesNumberResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; float min_value = 6; float max_value = 7; float step = 8; @@ -1049,7 +1078,7 @@ message ListEntitiesNumberResponse { string unit_of_measurement = 11; NumberMode mode = 12; string device_class = 13; - uint32 device_id = 14; + uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; } message NumberStateResponse { option (id) = 50; @@ -1063,7 +1092,7 @@ message NumberStateResponse { // If the number does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message NumberCommandRequest { option (id) = 51; @@ -1074,7 +1103,7 @@ message NumberCommandRequest { fixed32 key = 1; float state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== SELECT ==================== @@ -1087,13 +1116,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; - repeated string options = 6; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + repeated string options = 6 [(container_pointer) = "std::vector"]; bool disabled_by_default = 7; EntityCategory entity_category = 8; - uint32 device_id = 9; + uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message SelectStateResponse { option (id) = 53; @@ -1107,7 +1136,7 @@ message SelectStateResponse { // If the select does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message SelectCommandRequest { option (id) = 54; @@ -1118,7 +1147,7 @@ message SelectCommandRequest { fixed32 key = 1; string state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== SIREN ==================== @@ -1131,15 +1160,15 @@ message ListEntitiesSirenResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; repeated string tones = 7; bool supports_duration = 8; bool supports_volume = 9; EntityCategory entity_category = 10; - uint32 device_id = 11; + uint32 device_id = 11 [(field_ifdef) = "USE_DEVICES"]; } message SirenStateResponse { option (id) = 56; @@ -1150,7 +1179,7 @@ message SirenStateResponse { fixed32 key = 1; bool state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } message SirenCommandRequest { option (id) = 57; @@ -1168,7 +1197,7 @@ message SirenCommandRequest { uint32 duration = 7; bool has_volume = 8; float volume = 9; - uint32 device_id = 10; + uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } // ==================== LOCK ==================== @@ -1194,9 +1223,9 @@ message ListEntitiesLockResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; bool assumed_state = 8; @@ -1206,7 +1235,7 @@ message ListEntitiesLockResponse { // Not yet implemented: string code_format = 11; - uint32 device_id = 12; + uint32 device_id = 12 [(field_ifdef) = "USE_DEVICES"]; } message LockStateResponse { option (id) = 59; @@ -1216,7 +1245,7 @@ message LockStateResponse { option (no_delay) = true; fixed32 key = 1; LockState state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } message LockCommandRequest { option (id) = 60; @@ -1230,7 +1259,7 @@ message LockCommandRequest { // Not yet implemented: bool has_code = 3; string code = 4; - uint32 device_id = 5; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } // ==================== BUTTON ==================== @@ -1243,13 +1272,13 @@ message ListEntitiesButtonResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; - uint32 device_id = 9; + uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message ButtonCommandRequest { option (id) = 62; @@ -1259,7 +1288,7 @@ message ButtonCommandRequest { option (base_class) = "CommandProtoMessage"; fixed32 key = 1; - uint32 device_id = 2; + uint32 device_id = 2 [(field_ifdef) = "USE_DEVICES"]; } // ==================== MEDIA PLAYER ==================== @@ -1268,6 +1297,9 @@ enum MediaPlayerState { MEDIA_PLAYER_STATE_IDLE = 1; MEDIA_PLAYER_STATE_PLAYING = 2; MEDIA_PLAYER_STATE_PAUSED = 3; + MEDIA_PLAYER_STATE_ANNOUNCING = 4; + MEDIA_PLAYER_STATE_OFF = 5; + MEDIA_PLAYER_STATE_ON = 6; } enum MediaPlayerCommand { MEDIA_PLAYER_COMMAND_PLAY = 0; @@ -1275,6 +1307,15 @@ enum MediaPlayerCommand { MEDIA_PLAYER_COMMAND_STOP = 2; MEDIA_PLAYER_COMMAND_MUTE = 3; MEDIA_PLAYER_COMMAND_UNMUTE = 4; + MEDIA_PLAYER_COMMAND_TOGGLE = 5; + MEDIA_PLAYER_COMMAND_VOLUME_UP = 6; + MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7; + MEDIA_PLAYER_COMMAND_ENQUEUE = 8; + MEDIA_PLAYER_COMMAND_REPEAT_ONE = 9; + MEDIA_PLAYER_COMMAND_REPEAT_OFF = 10; + MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11; + MEDIA_PLAYER_COMMAND_TURN_ON = 12; + MEDIA_PLAYER_COMMAND_TURN_OFF = 13; } enum MediaPlayerFormatPurpose { MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0; @@ -1298,9 +1339,9 @@ message ListEntitiesMediaPlayerResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; @@ -1308,7 +1349,9 @@ message ListEntitiesMediaPlayerResponse { repeated MediaPlayerSupportedFormat supported_formats = 9; - uint32 device_id = 10; + uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; + + uint32 feature_flags = 11; } message MediaPlayerStateResponse { option (id) = 64; @@ -1320,7 +1363,7 @@ message MediaPlayerStateResponse { MediaPlayerState state = 2; float volume = 3; bool muted = 4; - uint32 device_id = 5; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } message MediaPlayerCommandRequest { option (id) = 65; @@ -1342,7 +1385,7 @@ message MediaPlayerCommandRequest { bool has_announcement = 8; bool announcement = 9; - uint32 device_id = 10; + uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } // ==================== BLUETOOTH ==================== @@ -1354,12 +1397,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"; @@ -1381,7 +1429,7 @@ message BluetoothLERawAdvertisement { sint32 rssi = 2; uint32 address_type = 3; - bytes data = 4; + bytes data = 4 [(fixed_array_size) = 62]; } message BluetoothLERawAdvertisementsResponse { @@ -1390,11 +1438,11 @@ message BluetoothLERawAdvertisementsResponse { option (ifdef) = "USE_BLUETOOTH_PROXY"; option (no_delay) = true; - repeated BluetoothLERawAdvertisement advertisements = 1; + repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"]; } enum BluetoothDeviceRequestType { - BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0; + BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0 [deprecated = true]; // V1 removed, use V3 variants BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1; BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR = 2; BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3; @@ -1434,21 +1482,39 @@ message BluetoothGATTGetServicesRequest { } message BluetoothGATTDescriptor { - repeated uint64 uuid = 1; + repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; uint32 handle = 2; + + // New field for efficient UUID (v1.12+) + // Only one of uuid or short_uuid will be set. + // short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients. + // 128-bit UUIDs always use the uuid field for backwards compatibility. + uint32 short_uuid = 3; // 16-bit or 32-bit UUID } message BluetoothGATTCharacteristic { - repeated uint64 uuid = 1; + repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; uint32 handle = 2; uint32 properties = 3; repeated BluetoothGATTDescriptor descriptors = 4; + + // New field for efficient UUID (v1.12+) + // Only one of uuid or short_uuid will be set. + // short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients. + // 128-bit UUIDs always use the uuid field for backwards compatibility. + uint32 short_uuid = 5; // 16-bit or 32-bit UUID } message BluetoothGATTService { - repeated uint64 uuid = 1; + repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; uint32 handle = 2; repeated BluetoothGATTCharacteristic characteristics = 3; + + // New field for efficient UUID (v1.12+) + // Only one of uuid or short_uuid will be set. + // short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients. + // 128-bit UUIDs always use the uuid field for backwards compatibility. + uint32 short_uuid = 4; // 16-bit or 32-bit UUID } message BluetoothGATTGetServicesResponse { @@ -1555,7 +1621,10 @@ message BluetoothConnectionsFreeResponse { uint32 free = 1; uint32 limit = 2; - repeated uint64 allocated = 3; + repeated uint64 allocated = 3 [ + (fixed_array_size_define) = "BLUETOOTH_PROXY_MAX_CONNECTIONS", + (fixed_array_skip_zero) = true + ]; } message BluetoothGATTErrorResponse { @@ -1800,7 +1869,7 @@ message VoiceAssistantConfigurationResponse { option (ifdef) = "USE_VOICE_ASSISTANT"; repeated VoiceAssistantWakeWord available_wake_words = 1; - repeated string active_wake_words = 2; + repeated string active_wake_words = 2 [(container_pointer) = "std::vector"]; uint32 max_active_wake_words = 3; } @@ -1845,14 +1914,14 @@ message ListEntitiesAlarmControlPanelResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; - string icon = 5; + reserved 4; // Deprecated: was string unique_id + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 supported_features = 8; bool requires_code = 9; bool requires_code_to_arm = 10; - uint32 device_id = 11; + uint32 device_id = 11 [(field_ifdef) = "USE_DEVICES"]; } message AlarmControlPanelStateResponse { @@ -1863,7 +1932,7 @@ message AlarmControlPanelStateResponse { option (no_delay) = true; fixed32 key = 1; AlarmControlPanelState state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } message AlarmControlPanelCommandRequest { @@ -1875,7 +1944,7 @@ message AlarmControlPanelCommandRequest { fixed32 key = 1; AlarmControlPanelStateCommand command = 2; string code = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } // ===================== TEXT ===================== @@ -1892,8 +1961,8 @@ message ListEntitiesTextResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; - string icon = 5; + reserved 4; // Deprecated: was string unique_id + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; @@ -1901,7 +1970,7 @@ message ListEntitiesTextResponse { uint32 max_length = 9; string pattern = 10; TextMode mode = 11; - uint32 device_id = 12; + uint32 device_id = 12 [(field_ifdef) = "USE_DEVICES"]; } message TextStateResponse { option (id) = 98; @@ -1915,7 +1984,7 @@ message TextStateResponse { // If the Text does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message TextCommandRequest { option (id) = 99; @@ -1926,7 +1995,7 @@ message TextCommandRequest { fixed32 key = 1; string state = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1940,12 +2009,12 @@ message ListEntitiesDateResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - uint32 device_id = 8; + uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; } message DateStateResponse { option (id) = 101; @@ -1961,7 +2030,7 @@ message DateStateResponse { uint32 year = 3; uint32 month = 4; uint32 day = 5; - uint32 device_id = 6; + uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"]; } message DateCommandRequest { option (id) = 102; @@ -1974,7 +2043,7 @@ message DateCommandRequest { uint32 year = 2; uint32 month = 3; uint32 day = 4; - uint32 device_id = 5; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } // ==================== DATETIME TIME ==================== @@ -1987,12 +2056,12 @@ message ListEntitiesTimeResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - uint32 device_id = 8; + uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; } message TimeStateResponse { option (id) = 104; @@ -2008,7 +2077,7 @@ message TimeStateResponse { uint32 hour = 3; uint32 minute = 4; uint32 second = 5; - uint32 device_id = 6; + uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"]; } message TimeCommandRequest { option (id) = 105; @@ -2021,7 +2090,7 @@ message TimeCommandRequest { uint32 hour = 2; uint32 minute = 3; uint32 second = 4; - uint32 device_id = 5; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } // ==================== EVENT ==================== @@ -2034,15 +2103,15 @@ message ListEntitiesEventResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; repeated string event_types = 9; - uint32 device_id = 10; + uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message EventResponse { option (id) = 108; @@ -2052,7 +2121,7 @@ message EventResponse { fixed32 key = 1; string event_type = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== VALVE ==================== @@ -2065,9 +2134,9 @@ message ListEntitiesValveResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; @@ -2075,7 +2144,7 @@ message ListEntitiesValveResponse { bool assumed_state = 9; bool supports_position = 10; bool supports_stop = 11; - uint32 device_id = 12; + uint32 device_id = 12 [(field_ifdef) = "USE_DEVICES"]; } enum ValveOperation { @@ -2093,7 +2162,7 @@ message ValveStateResponse { fixed32 key = 1; float position = 2; ValveOperation current_operation = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message ValveCommandRequest { @@ -2107,7 +2176,7 @@ message ValveCommandRequest { bool has_position = 2; float position = 3; bool stop = 4; - uint32 device_id = 5; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; } // ==================== DATETIME DATETIME ==================== @@ -2120,12 +2189,12 @@ message ListEntitiesDateTimeResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - uint32 device_id = 8; + uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; } message DateTimeStateResponse { option (id) = 113; @@ -2139,7 +2208,7 @@ message DateTimeStateResponse { // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; fixed32 epoch_seconds = 3; - uint32 device_id = 4; + uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; } message DateTimeCommandRequest { option (id) = 114; @@ -2150,7 +2219,7 @@ message DateTimeCommandRequest { fixed32 key = 1; fixed32 epoch_seconds = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } // ==================== UPDATE ==================== @@ -2163,13 +2232,13 @@ message ListEntitiesUpdateResponse { string object_id = 1; fixed32 key = 2; string name = 3; - string unique_id = 4; + reserved 4; // Deprecated: was string unique_id - string icon = 5; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; EntityCategory entity_category = 7; string device_class = 8; - uint32 device_id = 9; + uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message UpdateStateResponse { option (id) = 117; @@ -2188,7 +2257,7 @@ message UpdateStateResponse { string title = 8; string release_summary = 9; string release_url = 10; - uint32 device_id = 11; + uint32 device_id = 11 [(field_ifdef) = "USE_DEVICES"]; } enum UpdateCommand { UPDATE_COMMAND_NONE = 0; @@ -2204,5 +2273,5 @@ message UpdateCommandRequest { fixed32 key = 1; UpdateCommand command = 2; - uint32 device_id = 3; + uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ea3268a583..cdeabb5cac 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 @@ -25,8 +31,7 @@ #include "esphome/components/voice_assistant/voice_assistant.h" #endif -namespace esphome { -namespace api { +namespace esphome::api { // Read a maximum of 5 messages per loop iteration to prevent starving other components. // This is a balance between API responsiveness and allowing other components to run. @@ -42,32 +47,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 +112,11 @@ 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); + this->log_warning_("Helper init failed", err); 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,8 +143,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(), - api_error_to_str(err), errno); + this->log_socket_operation_failed_(err); return; } @@ -136,14 +159,7 @@ 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); - } + this->log_warning_("Reading failed", err); return; } else { this->last_traffic_ = now; @@ -186,9 +202,11 @@ void APIConnection::loop() { on_fatal_error(); ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); } - } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { + } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { + // Only send ping if we're not disconnecting ESP_LOGVV(TAG, "Sending keepalive PING"); - this->flags_.sent_ping = this->send_message(PingRequest()); + PingRequest req; + this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); if (!this->flags_.sent_ping) { // If we can't send the ping request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority @@ -203,24 +221,16 @@ void APIConnection::loop() { if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) { 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); - // partial message size calculated manually since its a special case - // 1 for the data field, varint for the data size, and the data itself - msg_size += 1 + ProtoSize::varint(to_send) + to_send; - ProtoSize::add_bool_field(msg_size, 1, done); - auto buffer = this->create_buffer(msg_size); - // fixed32 key = 1; - buffer.encode_fixed32(1, camera::Camera::instance()->get_object_id_hash()); - // bytes data = 2; - buffer.encode_bytes(2, this->image_reader_->peek_data_buffer(), to_send); - // bool done = 3; - buffer.encode_bool(3, done); + CameraImageResponse msg; + msg.key = camera::Camera::instance()->get_object_id_hash(); + msg.set_data(this->image_reader_->peek_data_buffer(), to_send); + msg.done = done; +#ifdef USE_DEVICES + msg.device_id = camera::Camera::instance()->get_device_id(); +#endif - bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE); - - if (success) { + if (this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { this->image_reader_->consume_data(to_send); if (done) { this->image_reader_->return_image(); @@ -229,35 +239,21 @@ void APIConnection::loop() { } #endif +#ifdef USE_API_HOMEASSISTANT_STATES if (state_subs_at_ >= 0) { - const auto &subs = this->parent_->get_state_subs(); - if (state_subs_at_ < static_cast(subs.size())) { - auto &it = subs[state_subs_at_]; - SubscribeHomeAssistantStateResponse resp; - resp.entity_id = it.entity_id; - resp.attribute = it.attribute.value(); - resp.once = it.once; - if (this->send_message(resp)) { - state_subs_at_++; - } - } else { - state_subs_at_ = -1; - } + this->process_state_subscriptions_(); } +#endif } -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) { +bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str()); this->flags_.next_close = true; DisconnectResponse resp; - return resp; + return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); } void APIConnection::on_disconnect_response(const DisconnectResponse &value) { this->helper_->close(); @@ -277,8 +273,9 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess #endif // Calculate size - uint32_t calculated_size = 0; - msg.calculate_size(calculated_size); + ProtoSize size_calc; + msg.calculate_size(size_calc); + uint32_t calculated_size = size_calc.get_size(); // Cache frame sizes to avoid repeated virtual calls const uint8_t header_padding = conn->helper_->frame_header_padding(); @@ -326,19 +323,18 @@ 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, bool is_single) { auto *binary_sensor = static_cast(entity); ListEntitiesBinarySensorResponse msg; - msg.device_class = binary_sensor->get_device_class(); + msg.set_device_class(binary_sensor->get_device_class_ref()); 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 @@ -352,14 +348,11 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * 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) { @@ -370,26 +363,12 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_position = traits.get_supports_position(); 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); + msg.set_device_class(cover->get_device_class_ref()); + 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) @@ -419,9 +398,8 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co if (traits.supports_direction()) msg.direction = static_cast(fan->direction); if (traits.supports_preset_modes()) - msg.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); + msg.set_preset_mode(StringRef(fan->preset_mode)); + return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -432,11 +410,8 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); - 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); + msg.supported_preset_modes = &traits.supported_preset_modes_for_api_(); + 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) @@ -479,26 +454,21 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * resp.color_temperature = values.get_color_temperature(); resp.cold_white = values.get_cold_white(); 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); + if (light->supports_effects()) { + // get_effect_name() returns temporary std::string - must store it + std::string effect_name = light->get_effect_name(); + resp.set_effect(StringRef(effect_name)); + } + 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) { auto *light = static_cast(entity); ListEntitiesLightResponse msg; 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) { + msg.supported_color_modes = &traits.get_supported_color_modes_for_api_(); + 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(); } @@ -508,9 +478,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c msg.effects.push_back(effect->get_name()); } } - msg.unique_id = get_default_unique_id("light", light); - fill_entity_info_base(light, msg); - return encode_message_to_buffer(msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::light_command(const LightCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) @@ -557,24 +526,20 @@ 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, bool is_single) { auto *sensor = static_cast(entity); ListEntitiesSensorResponse msg; - msg.unit_of_measurement = sensor->get_unit_of_measurement(); + msg.set_unit_of_measurement(sensor->get_unit_of_measurement_ref()); msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.force_update = sensor->get_force_update(); - msg.device_class = sensor->get_device_class(); + msg.set_device_class(sensor->get_device_class_ref()); msg.state_class = static_cast(sensor->get_state_class()); - msg.unique_id = sensor->unique_id(); - if (msg.unique_id.empty()) - msg.unique_id = get_default_unique_id("sensor", sensor); - fill_entity_info_base(sensor, msg); - return encode_message_to_buffer(msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } #endif @@ -589,8 +554,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, @@ -598,10 +563,9 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection * auto *a_switch = static_cast(entity); 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); + msg.set_device_class(a_switch->get_device_class_ref()); + 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) @@ -624,21 +588,18 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec bool is_single) { auto *text_sensor = static_cast(entity); TextSensorStateResponse resp; - resp.state = text_sensor->state; + resp.set_state(StringRef(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); + msg.set_device_class(text_sensor->get_device_class_ref()); + return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, + remaining_size, is_single); } #endif @@ -651,7 +612,6 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection bool is_single) { auto *climate = static_cast(entity); ClimateStateResponse resp; - fill_entity_state_base(climate, resp); auto traits = climate->get_traits(); resp.mode = static_cast(climate->mode); resp.action = static_cast(climate->action); @@ -665,20 +625,23 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection } if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); - if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) - resp.custom_fan_mode = climate->custom_fan_mode.value(); + if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) { + resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value())); + } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } - if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) - resp.custom_preset = climate->custom_preset.value(); + if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) { + resp.set_custom_preset(StringRef(climate->custom_preset.value())); + } if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); if (traits.get_supports_current_humidity()) 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) { @@ -689,29 +652,21 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supports_current_humidity = traits.get_supports_current_humidity(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); msg.supports_target_humidity = traits.get_supports_target_humidity(); - for (auto mode : traits.get_supported_modes()) - msg.supported_modes.push_back(static_cast(mode)); + msg.supported_modes = &traits.get_supported_modes_for_api_(); msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity(); - msg.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)); - for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) - msg.supported_custom_fan_modes.push_back(custom_fan_mode); - for (auto preset : traits.get_supported_presets()) - msg.supported_presets.push_back(static_cast(preset)); - for (auto const &custom_preset : traits.get_supported_custom_presets()) - 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); + msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_(); + msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_(); + msg.supported_presets = &traits.get_supported_presets_for_api_(); + msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_(); + msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_(); + return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) @@ -751,23 +706,21 @@ 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, bool is_single) { auto *number = static_cast(entity); ListEntitiesNumberResponse msg; - msg.unit_of_measurement = number->traits.get_unit_of_measurement(); + msg.set_unit_of_measurement(number->traits.get_unit_of_measurement_ref()); msg.mode = static_cast(number->traits.get_mode()); - msg.device_class = number->traits.get_device_class(); + msg.set_device_class(number->traits.get_device_class_ref()); msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - msg.unique_id = get_default_unique_id("number", number); - fill_entity_info_base(number, msg); - return encode_message_to_buffer(msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::number_command(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) @@ -789,16 +742,14 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c resp.year = date->year; resp.month = date->month; resp.day = date->day; - fill_entity_state_base(date, resp); - return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *date = static_cast(entity); ListEntitiesDateResponse msg; - msg.unique_id = get_default_unique_id("date", date); - fill_entity_info_base(date, msg); - return encode_message_to_buffer(msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::date_command(const DateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) @@ -820,16 +771,14 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c resp.hour = time->hour; resp.minute = time->minute; resp.second = time->second; - fill_entity_state_base(time, resp); - return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { auto *time = static_cast(entity); ListEntitiesTimeResponse msg; - msg.unique_id = get_default_unique_id("time", time); - fill_entity_info_base(time, msg); - return encode_message_to_buffer(msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::time_command(const TimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) @@ -852,16 +801,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) @@ -880,10 +828,9 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c bool is_single) { auto *text = static_cast(entity); TextStateResponse resp; - resp.state = text->state; + resp.set_state(StringRef(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, @@ -893,10 +840,9 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co msg.mode = static_cast(text->traits.get_mode()); 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); + msg.set_pattern(text->traits.get_pattern_ref()); + 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) @@ -915,21 +861,18 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection bool is_single) { auto *select = static_cast(entity); SelectStateResponse resp; - resp.state = select->state; + resp.set_state(StringRef(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, bool is_single) { auto *select = static_cast(entity); 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); + msg.options = &select->traits.get_options(); + 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) @@ -943,10 +886,9 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection * bool is_single) { 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); + msg.set_device_class(button->get_device_class_ref()); + return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) @@ -965,8 +907,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c auto *a_lock = static_cast(entity); LockStateResponse resp; resp.state = static_cast(a_lock->state); - fill_entity_state_base(a_lock, resp); - return encode_message_to_buffer(resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -976,9 +917,8 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co msg.assumed_state = a_lock->traits.get_assumed_state(); msg.supports_open = a_lock->traits.get_supports_open(); msg.requires_code = a_lock->traits.get_requires_code(); - msg.unique_id = get_default_unique_id("lock", a_lock); - fill_entity_info_base(a_lock, msg); - return encode_message_to_buffer(msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::lock_command(const LockCommandRequest &msg) { ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) @@ -1008,21 +948,19 @@ 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) { auto *valve = static_cast(entity); ListEntitiesValveResponse msg; auto traits = valve->get_traits(); - msg.device_class = valve->get_device_class(); + msg.set_device_class(valve->get_device_class_ref()); msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - msg.unique_id = get_default_unique_id("valve", valve); - fill_entity_info_base(valve, msg); - return encode_message_to_buffer(msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::valve_command(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) @@ -1049,8 +987,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) { @@ -1058,18 +996,18 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec ListEntitiesMediaPlayerResponse msg; auto traits = media_player->get_traits(); msg.supports_pause = traits.get_supports_pause(); + msg.feature_flags = traits.get_feature_flags(); for (auto &supported_format : traits.get_supported_formats()) { - MediaPlayerSupportedFormat media_format; - media_format.format = supported_format.format; + msg.supported_formats.emplace_back(); + auto &media_format = msg.supported_formats.back(); + media_format.set_format(StringRef(supported_format.format)); media_format.sample_rate = supported_format.sample_rate; media_format.num_channels = supported_format.num_channels; media_format.purpose = static_cast(supported_format.purpose); media_format.sample_bytes = supported_format.sample_bytes; - msg.supported_formats.push_back(media_format); } - msg.unique_id = get_default_unique_id("media_player", media_player); - fill_entity_info_base(media_player, msg); - return encode_message_to_buffer(msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, + remaining_size, is_single); } void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) @@ -1104,9 +1042,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) @@ -1130,6 +1067,12 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { } #endif +bool APIConnection::send_get_time_response(const GetTimeRequest &msg) { + GetTimeResponse resp; + resp.epoch_seconds = ::time(nullptr); + return this->send_message(resp, GetTimeResponse::MESSAGE_TYPE); +} + #ifdef USE_BLUETOOTH_PROXY void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); @@ -1137,21 +1080,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); } @@ -1175,12 +1103,10 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg); } -BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_free( +bool APIConnection::send_subscribe_bluetooth_connections_free_response( const SubscribeBluetoothConnectionsFreeRequest &msg) { - BluetoothConnectionsFreeResponse resp; - resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free(); - resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit(); - return resp; + bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this); + return true; } void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) { @@ -1241,28 +1167,25 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno } } -VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration( - const VoiceAssistantConfigurationRequest &msg) { +bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) { VoiceAssistantConfigurationResponse resp; if (!this->check_voice_assistant_api_connection_()) { - return resp; + return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); } auto &config = voice_assistant::global_voice_assistant->get_configuration(); for (auto &wake_word : config.available_wake_words) { - VoiceAssistantWakeWord resp_wake_word; - resp_wake_word.id = wake_word.id; - resp_wake_word.wake_word = wake_word.wake_word; + resp.available_wake_words.emplace_back(); + auto &resp_wake_word = resp.available_wake_words.back(); + resp_wake_word.set_id(StringRef(wake_word.id)); + resp_wake_word.set_wake_word(StringRef(wake_word.wake_word)); for (const auto &lang : wake_word.trained_languages) { resp_wake_word.trained_languages.push_back(lang); } - resp.available_wake_words.push_back(std::move(resp_wake_word)); - } - for (auto &wake_word_id : config.active_wake_words) { - resp.active_wake_words.push_back(wake_word_id); } + resp.active_wake_words = &config.active_wake_words; resp.max_active_wake_words = config.max_active_wake_words; - return resp; + return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); } void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { @@ -1284,8 +1207,8 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A auto *a_alarm_control_panel = static_cast(entity); AlarmControlPanelStateResponse resp; resp.state = static_cast(a_alarm_control_panel->get_state()); - fill_entity_state_base(a_alarm_control_panel, resp); - return encode_message_to_buffer(resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(a_alarm_control_panel, resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, + remaining_size, is_single); } uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1294,10 +1217,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) @@ -1337,21 +1258,19 @@ void APIConnection::send_event(event::Event *event, const std::string &event_typ 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); + resp.set_event_type(StringRef(event_type)); + 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, bool is_single) { auto *event = static_cast(entity); ListEntitiesEventResponse msg; - msg.device_class = event->get_device_class(); + msg.set_device_class(event->get_device_class_ref()); for (const auto &event_type : event->get_event_types()) msg.event_types.push_back(event_type); - msg.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 @@ -1371,23 +1290,21 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.has_progress = true; resp.progress = update->update_info.progress; } - resp.current_version = update->update_info.current_version; - resp.latest_version = update->update_info.latest_version; - resp.title = update->update_info.title; - resp.release_summary = update->update_info.summary; - resp.release_url = update->update_info.release_url; + resp.set_current_version(StringRef(update->update_info.current_version)); + resp.set_latest_version(StringRef(update->update_info.latest_version)); + resp.set_title(StringRef(update->update_info.title)); + resp.set_release_summary(StringRef(update->update_info.summary)); + resp.set_release_url(StringRef(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); + msg.set_device_class(update->get_device_class_ref()); + return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); } void APIConnection::update_command(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) @@ -1410,50 +1327,57 @@ 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; - - // Add size for level field (field ID 1, varint type) - // 1 byte for field tag + size of the level varint - msg_size += 1 + api::ProtoSize::varint(static_cast(level)); - - // Add size for string field (field ID 3, string type) - // 1 byte for field tag + size of length varint + string length - msg_size += 1 + api::ProtoSize::varint(static_cast(message_len)) + message_len; - - // Create a pre-sized buffer - auto buffer = this->create_buffer(msg_size); - - // Encode the message (SubscribeLogsResponse) - buffer.encode_uint32(1, static_cast(level)); // LogLevel level = 1 - buffer.encode_string(3, line, message_len); // string message = 3 - - // SubscribeLogsResponse - 29 - return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); + SubscribeLogsResponse msg; + msg.level = static_cast(level); + msg.set_message(reinterpret_cast(line), message_len); + return this->send_message_(msg, SubscribeLogsResponse::MESSAGE_TYPE); } -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()); +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 +} + +bool APIConnection::send_hello_response(const HelloRequest &msg) { + 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; - resp.api_version_minor = 10; - resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; - resp.name = App.get_name(); + resp.api_version_minor = 12; + // Temporary string for concatenation - will be valid during send_message call + std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; + resp.set_server_info(StringRef(server_info)); + resp.set_name(StringRef(App.get_name())); +#ifdef USE_API_PASSWORD + // Password required - wait for authentication this->flags_.connection_state = static_cast(ConnectionState::CONNECTED); - return resp; +#else + // No password configured - auto-authenticate + this->complete_authentication_(); +#endif + + return this->send_message(resp, HelloResponse::MESSAGE_TYPE); } -ConnectResponse APIConnection::connect(const ConnectRequest &msg) { +bool APIConnection::send_connect_response(const ConnectRequest &msg) { bool correct = true; #ifdef USE_API_PASSWORD correct = this->parent_->check_password(msg.password); @@ -1463,87 +1387,106 @@ 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; + return this->send_message(resp, ConnectResponse::MESSAGE_TYPE); } -DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { + +bool APIConnection::send_ping_response(const PingRequest &msg) { + PingResponse resp; + return this->send_message(resp, PingResponse::MESSAGE_TYPE); +} + +bool APIConnection::send_device_info_response(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(); - resp.suggested_area = App.get_area(); - resp.mac_address = get_mac_address_pretty(); - resp.esphome_version = ESPHOME_VERSION; - resp.compilation_time = App.get_compilation_time(); + resp.set_name(StringRef(App.get_name())); + resp.set_friendly_name(StringRef(App.get_friendly_name())); +#ifdef USE_AREAS + resp.set_suggested_area(StringRef(App.get_area())); +#endif + // mac_address must store temporary string - will be valid during send_message call + std::string mac_address = get_mac_address_pretty(); + resp.set_mac_address(StringRef(mac_address)); + + // Compile-time StringRef constants + static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); + resp.set_esphome_version(ESPHOME_VERSION_REF); + + // get_compilation_time() returns temporary std::string - must store it + std::string compilation_time = App.get_compilation_time(); + resp.set_compilation_time(StringRef(compilation_time)); + + // Compile-time StringRef constants for manufacturers #if defined(USE_ESP8266) || defined(USE_ESP32) - resp.manufacturer = "Espressif"; + static constexpr auto MANUFACTURER = StringRef::from_lit("Espressif"); #elif defined(USE_RP2040) - resp.manufacturer = "Raspberry Pi"; + static constexpr auto MANUFACTURER = StringRef::from_lit("Raspberry Pi"); #elif defined(USE_BK72XX) - resp.manufacturer = "Beken"; + static constexpr auto MANUFACTURER = StringRef::from_lit("Beken"); #elif defined(USE_LN882X) - resp.manufacturer = "Lightning"; + static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning"); #elif defined(USE_RTL87XX) - resp.manufacturer = "Realtek"; + static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek"); #elif defined(USE_HOST) - resp.manufacturer = "Host"; + static constexpr auto MANUFACTURER = StringRef::from_lit("Host"); #endif - resp.model = ESPHOME_BOARD; + resp.set_manufacturer(MANUFACTURER); + + static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD); + resp.set_model(MODEL); #ifdef USE_DEEP_SLEEP resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; #endif #ifdef ESPHOME_PROJECT_NAME - resp.project_name = ESPHOME_PROJECT_NAME; - resp.project_version = ESPHOME_PROJECT_VERSION; + static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME); + static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION); + resp.set_project_name(PROJECT_NAME); + resp.set_project_version(PROJECT_VERSION); #endif #ifdef USE_WEBSERVER 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(); + // bt_mac must store temporary string - will be valid during send_message call + std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); + resp.set_bluetooth_mac_address(StringRef(bluetooth_mac)); #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 resp.api_encryption_supported = true; #endif #ifdef USE_DEVICES + size_t device_index = 0; for (auto const &device : App.get_devices()) { - DeviceInfo device_info; + if (device_index >= ESPHOME_DEVICE_COUNT) + break; + auto &device_info = resp.devices[device_index++]; device_info.device_id = device->get_device_id(); - device_info.name = device->get_name(); + device_info.set_name(StringRef(device->get_name())); device_info.area_id = device->get_area_id(); - resp.devices.push_back(device_info); } #endif #ifdef USE_AREAS + size_t area_index = 0; for (auto const &area : App.get_areas()) { - AreaInfo area_info; + if (area_index >= ESPHOME_AREA_COUNT) + break; + auto &area_info = resp.areas[area_index++]; area_info.area_id = area->get_area_id(); - area_info.name = area->get_name(); - resp.areas.push_back(area_info); + area_info.set_name(StringRef(area->get_name())); } #endif - return resp; + + return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE); } + +#ifdef USE_API_HOMEASSISTANT_STATES void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { for (auto &it : this->parent_->get_state_subs()) { if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) { @@ -1551,6 +1494,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes } } } +#endif #ifdef USE_API_SERVICES void APIConnection::execute_service(const ExecuteServiceRequest &msg) { bool found = false; @@ -1565,28 +1509,27 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { } #endif #ifdef USE_API_NOISE -NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) { - psk_t psk{}; +bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) { NoiseEncryptionSetKeyResponse resp; + resp.success = false; + + psk_t psk{}; if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { ESP_LOGW(TAG, "Invalid encryption key length"); - resp.success = false; - return resp; - } - - if (!this->parent_->save_noise_psk(psk, true)) { + } else if (!this->parent_->save_noise_psk(psk, true)) { ESP_LOGW(TAG, "Failed to save encryption key"); - resp.success = false; - return resp; + } else { + resp.success = true; } - resp.success = true; - return resp; + return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE); } #endif +#ifdef USE_API_HOMEASSISTANT_STATES void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { state_subs_at_ = 0; } +#endif bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { if (this->flags_.remove) return false; @@ -1596,8 +1539,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(), - api_error_to_str(err), errno); + this->log_socket_operation_failed_(err); return false; } if (this->helper_->can_write_without_blocking()) @@ -1617,24 +1559,21 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { return false; if (err != APIError::OK) { on_fatal_error(); - if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { - ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); - } else { - ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), - api_error_to_str(err), errno); - } + this->log_warning_("Packet write failed", err); return false; } // Do not set last_traffic_ on send return true; } +#ifdef USE_API_PASSWORD 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()); } +#endif void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str()); + ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str()); } void APIConnection::on_fatal_error() { this->helper_->close(); @@ -1662,8 +1601,15 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { - // Insert at front for high priority messages (no deduplication check) - items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type, estimated_size)); + // Add high priority message and swap to front + // This avoids expensive vector::insert which shifts all elements + // Note: We only ever have one high-priority message at a time (ping OR disconnect) + // If we're disconnecting, pings are blocked, so this simple swap is sufficient + items.emplace_back(entity, std::move(creator), message_type, estimated_size); + if (items.size() > 1) { + // Swap the new high-priority item to the front + std::swap(items.front(), items.back()); + } } bool APIConnection::schedule_batch_() { @@ -1683,6 +1629,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; @@ -1694,6 +1644,8 @@ void APIConnection::process_batch_() { return; } + // Get shared buffer reference once to avoid multiple calls + auto &shared_buf = this->parent_->get_shared_buffer_ref(); size_t num_items = this->deferred_batch_.size(); // Fast path for single message - allocate exact size needed @@ -1704,8 +1656,7 @@ void APIConnection::process_batch_() { uint16_t payload_size = item.creator(item.entity, this, std::numeric_limits::max(), true, item.message_type); - if (payload_size > 0 && - this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { + if (payload_size > 0 && this->send_buffer(ProtoWriteBuffer{&shared_buf}, item.message_type)) { #ifdef HAS_PROTO_MESSAGE_DUMP // Log messages after send attempt for VV debugging // It's safe to use the buffer for logging at this point regardless of send result @@ -1720,29 +1671,30 @@ 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(); const uint8_t footer_size = this->helper_->frame_footer_size(); // Initialize buffer and tracking variables - this->parent_->get_shared_buffer_ref().clear(); + shared_buf.clear(); // Pre-calculate exact buffer size needed based on message types - uint32_t total_estimated_size = 0; + uint32_t total_estimated_size = num_items * (header_padding + footer_size); for (size_t i = 0; i < this->deferred_batch_.size(); i++) { const auto &item = this->deferred_batch_[i]; total_estimated_size += item.estimated_size; } // Calculate total overhead for all messages - uint32_t total_overhead = (header_padding + footer_size) * num_items; - // Reserve based on estimated size (much more accurate than 24-byte worst-case) - this->parent_->get_shared_buffer_ref().reserve(total_estimated_size + total_overhead); + shared_buf.reserve(total_estimated_size); this->flags_.batch_first_message = true; size_t items_processed = 0; @@ -1754,8 +1706,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 @@ -1769,7 +1721,11 @@ 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++; @@ -1780,7 +1736,7 @@ void APIConnection::process_batch_() { remaining_size -= payload_size; // Calculate where the next message's header padding will start // Current buffer size + footer space (that prepare_message_buffer will add for this message) - current_offset = this->parent_->get_shared_buffer_ref().size() + footer_size; + current_offset = shared_buf.size() + footer_size; } if (items_processed == 0) { @@ -1790,21 +1746,15 @@ void APIConnection::process_batch_() { // Add footer space for the last message (for Noise protocol MAC) if (footer_size > 0) { - auto &shared_buf = this->parent_->get_shared_buffer_ref(); shared_buf.resize(shared_buf.size() + footer_size); } // 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{&shared_buf}, + 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); - } + this->log_warning_("Batch write failed", err); } #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1860,6 +1810,33 @@ uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); } -} // namespace api -} // namespace esphome +#ifdef USE_API_HOMEASSISTANT_STATES +void APIConnection::process_state_subscriptions_() { + const auto &subs = this->parent_->get_state_subs(); + if (this->state_subs_at_ >= static_cast(subs.size())) { + this->state_subs_at_ = -1; + return; + } + + const auto &it = subs[this->state_subs_at_]; + SubscribeHomeAssistantStateResponse resp; + resp.set_entity_id(StringRef(it.entity_id)); + + // Avoid string copy by directly using the optional's value if it exists + resp.set_attribute(it.attribute.has_value() ? StringRef(it.attribute.value()) : StringRef("")); + + resp.once = it.once; + if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { + this->state_subs_at_++; + } +} +#endif // USE_API_HOMEASSISTANT_STATES + +void APIConnection::log_warning_(const char *message, APIError err) { + ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), message, api_error_to_str(err), errno); +} + +void APIConnection::log_socket_operation_failed_(APIError err) { this->log_warning_("Socket operation failed", err); } + +} // namespace esphome::api #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 391f4f088f..f0f308c248 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -13,13 +13,36 @@ #include #include -namespace esphome { -namespace api { +namespace esphome::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: @@ -108,15 +131,16 @@ class APIConnection : public APIServerConnection { void media_player_command(const MediaPlayerCommandRequest &msg) override; #endif bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len); +#ifdef USE_API_HOMEASSISTANT_SERVICES void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { if (!this->flags_.service_call_subscription) return; - this->send_message(call); + this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE); } +#endif #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; @@ -125,15 +149,14 @@ class APIConnection : public APIServerConnection { void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override; void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override; void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; - BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( - const SubscribeBluetoothConnectionsFreeRequest &msg) override; + bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override; void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override; #endif #ifdef USE_HOMEASSISTANT_TIME void send_time_request() { GetTimeRequest req; - this->send_message(req); + this->send_message(req, GetTimeRequest::MESSAGE_TYPE); } #endif @@ -144,8 +167,7 @@ class APIConnection : public APIServerConnection { void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; - VoiceAssistantConfigurationResponse voice_assistant_get_configuration( - const VoiceAssistantConfigurationRequest &msg) override; + bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override; void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; #endif @@ -168,15 +190,17 @@ class APIConnection : public APIServerConnection { // we initiated ping this->flags_.sent_ping = false; } +#ifdef USE_API_HOMEASSISTANT_STATES void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; +#endif #ifdef USE_HOMEASSISTANT_TIME void on_get_time_response(const GetTimeResponse &value) override; #endif - HelloResponse hello(const HelloRequest &msg) override; - ConnectResponse connect(const ConnectRequest &msg) override; - DisconnectResponse disconnect(const DisconnectRequest &msg) override; - PingResponse ping(const PingRequest &msg) override { return {}; } - DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; + bool send_hello_response(const HelloRequest &msg) override; + bool send_connect_response(const ConnectRequest &msg) override; + bool send_disconnect_response(const DisconnectRequest &msg) override; + bool send_ping_response(const PingRequest &msg) override; + bool send_device_info_response(const DeviceInfoRequest &msg) override; void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } void subscribe_states(const SubscribeStatesRequest &msg) override { this->flags_.state_subscription = true; @@ -187,19 +211,20 @@ class APIConnection : public APIServerConnection { if (msg.dump_config) App.schedule_dump_config(); } +#ifdef USE_API_HOMEASSISTANT_SERVICES void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override { this->flags_.service_call_subscription = true; } +#endif +#ifdef USE_API_HOMEASSISTANT_STATES void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; - GetTimeResponse get_time(const GetTimeRequest &msg) override { - // TODO - return {}; - } +#endif + bool send_get_time_response(const GetTimeRequest &msg) override; #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; + bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override; #endif bool is_authenticated() override { @@ -209,8 +234,18 @@ 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; } + + // Get client API version for feature detection + bool client_supports_api_version(uint16_t major, uint16_t minor) const { + return this->client_api_version_major_ > major || + (this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor); + } + void on_fatal_error() override; +#ifdef USE_API_PASSWORD void on_unauthenticated_access() override; +#endif void on_no_setup_connection() override; ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { // FIXME: ensure no recursive writes can happen @@ -260,49 +295,59 @@ class APIConnection : public APIServerConnection { bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; - std::string get_client_combined_info() const { - 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(); + // Helper function to handle authentication completion + void complete_authentication_(); - 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(); +#ifdef USE_API_HOMEASSISTANT_STATES + void process_state_subscriptions_(); #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 - } // Non-template helper to encode any ProtoMessage static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, uint32_t remaining_size, bool is_single); + // Helper to fill entity state base and encode message + static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type, + APIConnection *conn, uint32_t remaining_size, bool is_single) { + msg.key = entity->get_object_id_hash(); +#ifdef USE_DEVICES + msg.device_id = entity->get_device_id(); +#endif + return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); + } + + // Helper to fill entity info base and encode message + static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, + APIConnection *conn, uint32_t remaining_size, bool is_single) { + // Set common fields that are shared by all entity types + msg.key = entity->get_object_id_hash(); + // IMPORTANT: get_object_id() may return a temporary std::string + std::string object_id = entity->get_object_id(); + msg.set_object_id(StringRef(object_id)); + + if (entity->has_own_name()) { + msg.set_name(entity->get_name()); + } + + // Set common EntityBase properties +#ifdef USE_ENTITY_ICON + msg.set_icon(entity->get_icon_ref()); +#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; @@ -463,13 +508,14 @@ 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_; +#ifdef USE_API_HOMEASSISTANT_STATES int state_subs_at_ = -1; +#endif // Function pointer type for message encoding using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); @@ -703,8 +749,12 @@ class APIConnection : public APIServerConnection { this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type, estimated_size); return this->schedule_batch_(); } + + // Helper function to log API errors with errno + void log_warning_(const char *message, APIError err); + // Specific helper for duplicated error message + void log_socket_operation_failed_(APIError err); }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index afd64e8981..dee3af2ac3 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -1,5 +1,6 @@ #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" @@ -8,10 +9,19 @@ #include #include -namespace esphome { -namespace api { +namespace esphome::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 @@ -19,8 +29,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) { @@ -41,6 +49,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) { @@ -51,17 +67,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"; } @@ -76,33 +89,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()) { @@ -115,46 +150,27 @@ 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 } } // Try to send directly if no buffered data - ssize_t sent = this->socket_->writev(iov, iovcnt); + // Optimize for single iovec case (common for plaintext API) + ssize_t sent = + (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt); if (sent == -1) { - 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 @@ -173,14 +189,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; @@ -203,13 +212,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; } @@ -217,14 +226,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) { @@ -240,840 +247,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(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()); - - // 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(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()); - - 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 +} // namespace esphome::api #endif diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 4bcc4acd61..76dfe1366c 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -8,16 +8,17 @@ #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 { +namespace esphome::api { + +// uncomment to log raw packets +//#define HELPER_LOG_PACKETS + +// Forward declaration +struct ClientInfo; class ProtoWriteBuffer; @@ -40,7 +41,6 @@ struct PacketInfo { 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, @@ -51,16 +51,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); @@ -68,7 +71,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; @@ -94,8 +98,6 @@ 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(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 @@ -109,29 +111,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); @@ -161,10 +162,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}; @@ -179,105 +183,6 @@ 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(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_; } +} // namespace esphome::api - 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(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_(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..35d1715931 --- /dev/null +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -0,0 +1,583 @@ +#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::api { + +static const char *const TAG = "api.noise"; +static const char *const PROLOGUE_INIT = "NoiseAPIInit"; +static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") + +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) + +#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 + size_t old_size = prologue_.size(); + prologue_.resize(old_size + PROLOGUE_INIT_LEN); + std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); + + 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 + // Resize for: existing prologue + 2 size bytes + frame data + size_t old_size = prologue_.size(); + prologue_.resize(old_size + 2 + frame.size()); + prologue_[old_size] = (uint8_t) (frame.size() >> 8); + prologue_[old_size + 1] = (uint8_t) frame.size(); + std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size()); + + 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; + // Calculate positions and sizes + size_t name_len = name.size() + 1; // including null terminator + size_t mac_len = mac.size() + 1; // including null terminator + size_t name_offset = 1; + size_t mac_offset = name_offset + name_len; + size_t total_size = 1 + name_len + mac_len; + + msg.resize(total_size); + + // chosen proto + msg[0] = 0x01; + + // node name, terminated by null byte + std::memcpy(msg.data() + name_offset, name.c_str(), name_len); + // node mac, terminated by null byte + std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len); + + 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 esphome::api +#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..e82e5daadb --- /dev/null +++ b/esphome/components/api/api_frame_helper_noise.h @@ -0,0 +1,68 @@ +#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::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 esphome::api +#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..fdaacbd94e --- /dev/null +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -0,0 +1,290 @@ +#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::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 esphome::api +#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..b50902dd75 --- /dev/null +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -0,0 +1,53 @@ +#pragma once +#include "api_frame_helper.h" +#ifdef USE_API +#ifdef USE_API_PLAINTEXT + +namespace esphome::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 esphome::api +#endif // USE_API_PLAINTEXT +#endif // USE_API diff --git a/esphome/components/api/api_noise_context.h b/esphome/components/api/api_noise_context.h index fa4435e570..b5f7016689 100644 --- a/esphome/components/api/api_noise_context.h +++ b/esphome/components/api/api_noise_context.h @@ -3,8 +3,7 @@ #include #include "esphome/core/defines.h" -namespace esphome { -namespace api { +namespace esphome::api { #ifdef USE_API_NOISE using psk_t = std::array; @@ -28,5 +27,4 @@ class APINoiseContext { }; #endif // USE_API_NOISE -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 3a547b8688..50c43b96fd 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -23,3 +23,38 @@ 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; + optional bool no_zero_copy = 50008 [default=false]; + optional bool fixed_array_skip_zero = 50009 [default=false]; + optional string fixed_array_size_define = 50010; + optional string fixed_array_with_length_define = 50011; + + // container_pointer: Zero-copy optimization for repeated fields. + // + // When container_pointer is set on a repeated field, the generated message will + // store a pointer to an existing container instead of copying the data into the + // message's own repeated field. This eliminates heap allocations and improves performance. + // + // Requirements for safe usage: + // 1. The source container must remain valid until the message is encoded + // 2. Messages must be encoded immediately (which ESPHome does by default) + // 3. The container type must match the field type exactly + // + // Supported container types: + // - "std::vector" for most repeated fields + // - "std::set" for unique/sorted data + // - Full type specification required for enums (e.g., "std::set") + // + // Example usage in .proto file: + // repeated string supported_modes = 12 [(container_pointer) = "std::set"]; + // repeated ColorMode color_modes = 13 [(container_pointer) = "std::set"]; + // + // The corresponding C++ code must provide const reference access to a container + // that matches the specified type and remains valid during message encoding. + // This is typically done through methods returning const T& or special accessor + // methods like get_options() or supported_modes_for_api_(). + optional string container_pointer = 50001; +} diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 4c0e20e0f0..476e3c88d0 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3,488 +3,495 @@ #include "api_pb2.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include -namespace esphome { -namespace api { +namespace esphome::api { bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->api_version_major = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->api_version_minor = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->client_info = value.as_string(); - return true; - } + break; default: return false; } + return true; } void HelloResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->api_version_major); buffer.encode_uint32(2, this->api_version_minor); - buffer.encode_string(3, this->server_info); - buffer.encode_string(4, this->name); + buffer.encode_string(3, this->server_info_ref_); + buffer.encode_string(4, this->name_ref_); } -void HelloResponse::calculate_size(uint32_t &total_size) const { - 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); +void HelloResponse::calculate_size(ProtoSize &size) const { + size.add_uint32(1, this->api_version_major); + size.add_uint32(1, this->api_version_minor); + size.add_length(1, this->server_info_ref_.size()); + size.add_length(1, this->name_ref_.size()); } bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->password = value.as_string(); - return true; - } + break; default: return false; } + return true; } void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); } -void ConnectResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_bool_field(total_size, 1, this->invalid_password); -} -bool AreaInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->area_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool AreaInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->name = value.as_string(); - return true; - } - default: - return false; - } -} +void ConnectResponse::calculate_size(ProtoSize &size) const { size.add_bool(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); + buffer.encode_string(2, this->name_ref_); } -void AreaInfo::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint32_field(total_size, 1, this->area_id); - ProtoSize::add_string_field(total_size, 1, this->name); -} -bool DeviceInfo::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->device_id = value.as_uint32(); - return true; - } - case 3: { - this->area_id = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool DeviceInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 2: { - this->name = value.as_string(); - return true; - } - default: - return false; - } +void AreaInfo::calculate_size(ProtoSize &size) const { + size.add_uint32(1, this->area_id); + size.add_length(1, this->name_ref_.size()); } +#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_string(2, this->name_ref_); 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); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_uint32_field(total_size, 1, this->area_id); +void DeviceInfo::calculate_size(ProtoSize &size) const { + size.add_uint32(1, this->device_id); + size.add_length(1, this->name_ref_.size()); + size.add_uint32(1, this->area_id); } +#endif void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { +#ifdef USE_API_PASSWORD buffer.encode_bool(1, this->uses_password); - 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); +#endif + buffer.encode_string(2, this->name_ref_); + buffer.encode_string(3, this->mac_address_ref_); + buffer.encode_string(4, this->esphome_version_ref_); + buffer.encode_string(5, this->compilation_time_ref_); + buffer.encode_string(6, this->model_ref_); +#ifdef USE_DEEP_SLEEP buffer.encode_bool(7, this->has_deep_sleep); - buffer.encode_string(8, this->project_name); - buffer.encode_string(9, this->project_version); +#endif +#ifdef ESPHOME_PROJECT_NAME + buffer.encode_string(8, this->project_name_ref_); +#endif +#ifdef ESPHOME_PROJECT_NAME + buffer.encode_string(9, this->project_version_ref_); +#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); - buffer.encode_string(12, this->manufacturer); - buffer.encode_string(13, this->friendly_name); - buffer.encode_uint32(14, this->legacy_voice_assistant_version); +#endif + buffer.encode_string(12, this->manufacturer_ref_); + buffer.encode_string(13, this->friendly_name_ref_); +#ifdef USE_VOICE_ASSISTANT buffer.encode_uint32(17, this->voice_assistant_feature_flags); - buffer.encode_string(16, this->suggested_area); - buffer.encode_string(18, this->bluetooth_mac_address); +#endif +#ifdef USE_AREAS + buffer.encode_string(16, this->suggested_area_ref_); +#endif +#ifdef USE_BLUETOOTH_PROXY + buffer.encode_string(18, this->bluetooth_mac_address_ref_); +#endif +#ifdef USE_API_NOISE buffer.encode_bool(19, this->api_encryption_supported); - for (auto &it : this->devices) { +#endif +#ifdef USE_DEVICES + for (const auto &it : this->devices) { buffer.encode_message(20, it, true); } - for (auto &it : this->areas) { +#endif +#ifdef USE_AREAS + for (const auto &it : this->areas) { buffer.encode_message(21, it, true); } +#endif +#ifdef USE_AREAS buffer.encode_message(22, this->area); +#endif } -void DeviceInfoResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_bool_field(total_size, 1, this->uses_password); - 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); - ProtoSize::add_bool_field(total_size, 1, this->has_deep_sleep); - ProtoSize::add_string_field(total_size, 1, this->project_name); - ProtoSize::add_string_field(total_size, 1, this->project_version); - ProtoSize::add_uint32_field(total_size, 1, this->webserver_port); - ProtoSize::add_uint32_field(total_size, 1, this->legacy_bluetooth_proxy_version); - ProtoSize::add_uint32_field(total_size, 1, this->bluetooth_proxy_feature_flags); - ProtoSize::add_string_field(total_size, 1, this->manufacturer); - ProtoSize::add_string_field(total_size, 1, this->friendly_name); - ProtoSize::add_uint32_field(total_size, 1, this->legacy_voice_assistant_version); - ProtoSize::add_uint32_field(total_size, 2, this->voice_assistant_feature_flags); - ProtoSize::add_string_field(total_size, 2, this->suggested_area); - ProtoSize::add_string_field(total_size, 2, this->bluetooth_mac_address); - ProtoSize::add_bool_field(total_size, 2, this->api_encryption_supported); - ProtoSize::add_repeated_message(total_size, 2, this->devices); - ProtoSize::add_repeated_message(total_size, 2, this->areas); - ProtoSize::add_message_object(total_size, 2, this->area); +void DeviceInfoResponse::calculate_size(ProtoSize &size) const { +#ifdef USE_API_PASSWORD + size.add_bool(1, this->uses_password); +#endif + size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->mac_address_ref_.size()); + size.add_length(1, this->esphome_version_ref_.size()); + size.add_length(1, this->compilation_time_ref_.size()); + size.add_length(1, this->model_ref_.size()); +#ifdef USE_DEEP_SLEEP + size.add_bool(1, this->has_deep_sleep); +#endif +#ifdef ESPHOME_PROJECT_NAME + size.add_length(1, this->project_name_ref_.size()); +#endif +#ifdef ESPHOME_PROJECT_NAME + size.add_length(1, this->project_version_ref_.size()); +#endif +#ifdef USE_WEBSERVER + size.add_uint32(1, this->webserver_port); +#endif +#ifdef USE_BLUETOOTH_PROXY + size.add_uint32(1, this->bluetooth_proxy_feature_flags); +#endif + size.add_length(1, this->manufacturer_ref_.size()); + size.add_length(1, this->friendly_name_ref_.size()); +#ifdef USE_VOICE_ASSISTANT + size.add_uint32(2, this->voice_assistant_feature_flags); +#endif +#ifdef USE_AREAS + size.add_length(2, this->suggested_area_ref_.size()); +#endif +#ifdef USE_BLUETOOTH_PROXY + size.add_length(2, this->bluetooth_mac_address_ref_.size()); +#endif +#ifdef USE_API_NOISE + size.add_bool(2, this->api_encryption_supported); +#endif +#ifdef USE_DEVICES + for (const auto &it : this->devices) { + size.add_message_object_force(2, it); + } +#endif +#ifdef USE_AREAS + for (const auto &it : this->areas) { + size.add_message_object_force(2, it); + } +#endif +#ifdef USE_AREAS + size.add_message_object(2, this->area); +#endif } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); 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_string(3, this->name_ref_); + buffer.encode_string(5, this->device_class_ref_); buffer.encode_bool(6, this->is_status_binary_sensor); buffer.encode_bool(7, this->disabled_by_default); - buffer.encode_string(8, this->icon); +#ifdef USE_ENTITY_ICON + buffer.encode_string(8, this->icon_ref_); +#endif buffer.encode_uint32(9, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); +#endif } -void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->device_class); - ProtoSize::add_bool_field(total_size, 1, this->is_status_binary_sensor); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); - ProtoSize::add_string_field(total_size, 1, this->icon); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void ListEntitiesBinarySensorResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->device_class_ref_.size()); + size.add_bool(1, this->is_status_binary_sensor); + size.add_bool(1, this->disabled_by_default); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_uint32(1, static_cast(this->entity_category)); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } -void BinarySensorStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_bool_field(total_size, 1, this->state); - ProtoSize::add_bool_field(total_size, 1, this->missing_state); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void BinarySensorStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_bool(1, this->state); + size.add_bool(1, this->missing_state); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } #endif #ifdef USE_COVER void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); + buffer.encode_string(3, this->name_ref_); 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_string(8, this->device_class_ref_); buffer.encode_bool(9, this->disabled_by_default); - buffer.encode_string(10, this->icon); +#ifdef USE_ENTITY_ICON + buffer.encode_string(10, this->icon_ref_); +#endif buffer.encode_uint32(11, static_cast(this->entity_category)); buffer.encode_bool(12, this->supports_stop); +#ifdef USE_DEVICES buffer.encode_uint32(13, this->device_id); +#endif } -void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_bool_field(total_size, 1, this->assumed_state); - ProtoSize::add_bool_field(total_size, 1, this->supports_position); - ProtoSize::add_bool_field(total_size, 1, this->supports_tilt); - ProtoSize::add_string_field(total_size, 1, this->device_class); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); - ProtoSize::add_string_field(total_size, 1, this->icon); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - ProtoSize::add_bool_field(total_size, 1, this->supports_stop); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void ListEntitiesCoverResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); + size.add_bool(1, this->assumed_state); + size.add_bool(1, this->supports_position); + size.add_bool(1, this->supports_tilt); + size.add_length(1, this->device_class_ref_.size()); + size.add_bool(1, this->disabled_by_default); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_uint32(1, static_cast(this->entity_category)); + size.add_bool(1, this->supports_stop); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_uint32(2, static_cast(this->legacy_state)); buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); buffer.encode_uint32(5, static_cast(this->current_operation)); +#ifdef USE_DEVICES buffer.encode_uint32(6, this->device_id); +#endif } -void CoverStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->legacy_state)); - ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->tilt != 0.0f); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation)); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void CoverStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_float(1, this->position); + size.add_float(1, this->tilt); + size.add_uint32(1, static_cast(this->current_operation)); +#ifdef USE_DEVICES + size.add_uint32(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 = static_cast(value.as_uint32()); - 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; - } - case 9: { + break; +#ifdef USE_DEVICES + case 9: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 5: { + break; + case 5: this->position = value.as_float(); - return true; - } - case 7: { + break; + case 7: this->tilt = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_FAN void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); + buffer.encode_string(3, this->name_ref_); 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); - buffer.encode_string(10, this->icon); +#ifdef USE_ENTITY_ICON + buffer.encode_string(10, this->icon_ref_); +#endif buffer.encode_uint32(11, static_cast(this->entity_category)); - for (auto &it : this->supported_preset_modes) { + for (const auto &it : *this->supported_preset_modes) { buffer.encode_string(12, it, true); } +#ifdef USE_DEVICES buffer.encode_uint32(13, this->device_id); +#endif } -void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_bool_field(total_size, 1, this->supports_oscillation); - ProtoSize::add_bool_field(total_size, 1, this->supports_speed); - ProtoSize::add_bool_field(total_size, 1, this->supports_direction); - ProtoSize::add_int32_field(total_size, 1, this->supported_speed_count); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); - ProtoSize::add_string_field(total_size, 1, this->icon); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - if (!this->supported_preset_modes.empty()) { - for (const auto &it : this->supported_preset_modes) { - ProtoSize::add_string_field_repeated(total_size, 1, it); +void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); + size.add_bool(1, this->supports_oscillation); + size.add_bool(1, this->supports_speed); + size.add_bool(1, this->supports_direction); + size.add_int32(1, this->supported_speed_count); + size.add_bool(1, this->disabled_by_default); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_uint32(1, static_cast(this->entity_category)); + if (!this->supported_preset_modes->empty()) { + for (const auto &it : *this->supported_preset_modes) { + size.add_length_force(1, it.size()); } } - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#ifdef USE_DEVICES + size.add_uint32(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_uint32(4, static_cast(this->speed)); buffer.encode_uint32(5, static_cast(this->direction)); buffer.encode_int32(6, this->speed_level); - buffer.encode_string(7, this->preset_mode); + buffer.encode_string(7, this->preset_mode_ref_); +#ifdef USE_DEVICES buffer.encode_uint32(8, this->device_id); +#endif } -void FanStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_bool_field(total_size, 1, this->state); - ProtoSize::add_bool_field(total_size, 1, this->oscillating); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->speed)); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->direction)); - ProtoSize::add_int32_field(total_size, 1, this->speed_level); - ProtoSize::add_string_field(total_size, 1, this->preset_mode); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void FanStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_bool(1, this->state); + size.add_bool(1, this->oscillating); + size.add_uint32(1, static_cast(this->direction)); + size.add_int32(1, this->speed_level); + size.add_length(1, this->preset_mode_ref_.size()); +#ifdef USE_DEVICES + size.add_uint32(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 = static_cast(value.as_uint32()); - return true; - } - case 6: { + break; + case 6: this->has_oscillating = value.as_bool(); - return true; - } - case 7: { + break; + case 7: this->oscillating = value.as_bool(); - return true; - } - case 8: { + break; + case 8: this->has_direction = value.as_bool(); - return true; - } - case 9: { + break; + case 9: this->direction = static_cast(value.as_uint32()); - return true; - } - case 10: { + break; + case 10: this->has_speed_level = value.as_bool(); - return true; - } - case 11: { + break; + case 11: this->speed_level = value.as_int32(); - return true; - } - case 12: { + break; + case 12: this->has_preset_mode = value.as_bool(); - return true; - } - case 14: { + break; +#ifdef USE_DEVICES + case 14: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 13: { + case 13: this->preset_mode = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_LIGHT void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); 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_string(3, this->name_ref_); + for (const auto &it : *this->supported_color_modes) { 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); - buffer.encode_string(14, this->icon); +#ifdef USE_ENTITY_ICON + buffer.encode_string(14, this->icon_ref_); +#endif buffer.encode_uint32(15, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(16, this->device_id); +#endif } -void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - if (!this->supported_color_modes.empty()) { - for (const auto &it : this->supported_color_modes) { - ProtoSize::add_enum_field_repeated(total_size, 1, static_cast(it)); +void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); + if (!this->supported_color_modes->empty()) { + for (const auto &it : *this->supported_color_modes) { + size.add_uint32_force(1, static_cast(it)); } } - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_brightness); - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_rgb); - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_white_value); - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_color_temperature); - ProtoSize::add_fixed_field<4>(total_size, 1, this->min_mireds != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->max_mireds != 0.0f); + size.add_float(1, this->min_mireds); + size.add_float(1, this->max_mireds); if (!this->effects.empty()) { for (const auto &it : this->effects) { - ProtoSize::add_string_field_repeated(total_size, 1, it); + size.add_length_force(1, it.size()); } } - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); - ProtoSize::add_string_field(total_size, 1, this->icon); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - ProtoSize::add_uint32_field(total_size, 2, this->device_id); + size.add_bool(1, this->disabled_by_default); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_uint32(1, static_cast(this->entity_category)); +#ifdef USE_DEVICES + size.add_uint32(2, this->device_id); +#endif } void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -499,362 +506,354 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(8, this->color_temperature); buffer.encode_float(12, this->cold_white); buffer.encode_float(13, this->warm_white); - buffer.encode_string(9, this->effect); + buffer.encode_string(9, this->effect_ref_); +#ifdef USE_DEVICES buffer.encode_uint32(14, this->device_id); +#endif } -void LightStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_bool_field(total_size, 1, this->state); - ProtoSize::add_fixed_field<4>(total_size, 1, this->brightness != 0.0f); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->color_mode)); - ProtoSize::add_fixed_field<4>(total_size, 1, this->color_brightness != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->red != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->green != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->blue != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->white != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->color_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->cold_white != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->warm_white != 0.0f); - ProtoSize::add_string_field(total_size, 1, this->effect); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void LightStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_bool(1, this->state); + size.add_float(1, this->brightness); + size.add_uint32(1, static_cast(this->color_mode)); + size.add_float(1, this->color_brightness); + size.add_float(1, this->red); + size.add_float(1, this->green); + size.add_float(1, this->blue); + size.add_float(1, this->white); + size.add_float(1, this->color_temperature); + size.add_float(1, this->cold_white); + size.add_float(1, this->warm_white); + size.add_length(1, this->effect_ref_.size()); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_state = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->state = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->has_brightness = value.as_bool(); - return true; - } - case 22: { + break; + case 22: this->has_color_mode = value.as_bool(); - return true; - } - case 23: { + break; + case 23: this->color_mode = static_cast(value.as_uint32()); - return true; - } - case 20: { + break; + case 20: this->has_color_brightness = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_rgb = value.as_bool(); - return true; - } - case 10: { + break; + case 10: this->has_white = value.as_bool(); - return true; - } - case 12: { + break; + case 12: this->has_color_temperature = value.as_bool(); - return true; - } - case 24: { + break; + case 24: this->has_cold_white = value.as_bool(); - return true; - } - case 26: { + break; + case 26: this->has_warm_white = value.as_bool(); - return true; - } - case 14: { + break; + case 14: this->has_transition_length = value.as_bool(); - return true; - } - case 15: { + break; + case 15: this->transition_length = value.as_uint32(); - return true; - } - case 16: { + break; + case 16: this->has_flash_length = value.as_bool(); - return true; - } - case 17: { + break; + case 17: this->flash_length = value.as_uint32(); - return true; - } - case 18: { + break; + case 18: this->has_effect = value.as_bool(); - return true; - } - case 28: { + break; +#ifdef USE_DEVICES + case 28: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 19: { + case 19: this->effect = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 5: { + break; + case 5: this->brightness = value.as_float(); - return true; - } - case 21: { + break; + case 21: this->color_brightness = value.as_float(); - return true; - } - case 7: { + break; + case 7: this->red = value.as_float(); - return true; - } - case 8: { + break; + case 8: this->green = value.as_float(); - return true; - } - case 9: { + break; + case 9: this->blue = value.as_float(); - return true; - } - case 11: { + break; + case 11: this->white = value.as_float(); - return true; - } - case 13: { + break; + case 13: this->color_temperature = value.as_float(); - return true; - } - case 25: { + break; + case 25: this->cold_white = value.as_float(); - return true; - } - case 27: { + break; + case 27: this->warm_white = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_SENSOR void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); - buffer.encode_string(6, this->unit_of_measurement); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif + buffer.encode_string(6, this->unit_of_measurement_ref_); buffer.encode_int32(7, this->accuracy_decimals); buffer.encode_bool(8, this->force_update); - buffer.encode_string(9, this->device_class); + buffer.encode_string(9, this->device_class_ref_); buffer.encode_uint32(10, static_cast(this->state_class)); - buffer.encode_uint32(11, static_cast(this->legacy_last_reset_type)); buffer.encode_bool(12, this->disabled_by_default); buffer.encode_uint32(13, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(14, this->device_id); +#endif } -void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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_enum_field(total_size, 1, static_cast(this->legacy_last_reset_type)); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void ListEntitiesSensorResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_length(1, this->unit_of_measurement_ref_.size()); + size.add_int32(1, this->accuracy_decimals); + size.add_bool(1, this->force_update); + size.add_length(1, this->device_class_ref_.size()); + size.add_uint32(1, static_cast(this->state_class)); + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } -void SensorStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f); - ProtoSize::add_bool_field(total_size, 1, this->missing_state); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void SensorStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_float(1, this->state); + size.add_bool(1, this->missing_state); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } #endif #ifdef USE_SWITCH void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->assumed_state); buffer.encode_bool(7, this->disabled_by_default); buffer.encode_uint32(8, static_cast(this->entity_category)); - buffer.encode_string(9, this->device_class); + buffer.encode_string(9, this->device_class_ref_); +#ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); +#endif } -void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void ListEntitiesSwitchResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->assumed_state); + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); + size.add_length(1, this->device_class_ref_.size()); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); +#ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); +#endif } -void SwitchStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_bool_field(total_size, 1, this->state); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void SwitchStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_bool(1, this->state); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->state = value.as_bool(); - return true; - } - case 3: { + break; +#ifdef USE_DEVICES + case 3: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_TEXT_SENSOR void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + buffer.encode_string(8, this->device_class_ref_); +#ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); +#endif } -void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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_uint32_field(total_size, 1, this->device_id); +void ListEntitiesTextSensorResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); + size.add_length(1, this->device_class_ref_.size()); +#ifdef USE_DEVICES + size.add_uint32(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_string(2, this->state_ref_); buffer.encode_bool(3, this->missing_state); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } -void TextSensorStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->state); - ProtoSize::add_bool_field(total_size, 1, this->missing_state); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void TextSensorStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_length(1, this->state_ref_.size()); + size.add_bool(1, this->missing_state); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } #endif bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->level = static_cast(value.as_uint32()); - return true; - } - case 2: { + break; + case 2: this->dump_config = value.as_bool(); - return true; - } + break; default: return false; } + return true; } void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, static_cast(this->level)); - buffer.encode_bytes(3, reinterpret_cast(this->message.data()), this->message.size()); - buffer.encode_bool(4, this->send_failed); + buffer.encode_bytes(3, this->message_ptr_, this->message_len_); } -void SubscribeLogsResponse::calculate_size(uint32_t &total_size) const { - 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); +void SubscribeLogsResponse::calculate_size(ProtoSize &size) const { + size.add_uint32(1, static_cast(this->level)); + size.add_length(1, this->message_len_); } #ifdef USE_API_NOISE bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_string(); - return true; - } + break; default: return false; } + return true; } void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } -void NoiseEncryptionSetKeyResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_bool_field(total_size, 1, this->success); -} +void NoiseEncryptionSetKeyResponse::calculate_size(ProtoSize &size) const { size.add_bool(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; - } -} +#ifdef USE_API_HOMEASSISTANT_SERVICES void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->key); + buffer.encode_string(1, this->key_ref_); buffer.encode_string(2, this->value); } -void HomeassistantServiceMap::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->key); - ProtoSize::add_string_field(total_size, 1, this->value); +void HomeassistantServiceMap::calculate_size(ProtoSize &size) const { + size.add_length(1, this->key_ref_.size()); + size.add_length(1, this->value.size()); } void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->service); + buffer.encode_string(1, this->service_ref_); for (auto &it : this->data) { buffer.encode_message(2, it, true); } @@ -866,356 +865,298 @@ void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { } 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); - 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); +void HomeassistantServiceResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->service_ref_.size()); + size.add_repeated_message(1, this->data); + size.add_repeated_message(1, this->data_template); + size.add_repeated_message(1, this->variables); + size.add_bool(1, this->is_event); } +#endif +#ifdef USE_API_HOMEASSISTANT_STATES void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->entity_id); - buffer.encode_string(2, this->attribute); + buffer.encode_string(1, this->entity_id_ref_); + buffer.encode_string(2, this->attribute_ref_); 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); - ProtoSize::add_string_field(total_size, 1, this->attribute); - ProtoSize::add_bool_field(total_size, 1, this->once); +void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->entity_id_ref_.size()); + size.add_length(1, this->attribute_ref_.size()); + size.add_bool(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; } + return true; } +#endif 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); -} +void GetTimeResponse::calculate_size(ProtoSize &size) const { size.add_fixed32(1, this->epoch_seconds); } #ifdef USE_API_SERVICES -bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->type = static_cast(value.as_uint32()); - return true; - } - default: - return false; - } -} -bool ListEntitiesServicesArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->name = value.as_string(); - return true; - } - default: - return false; - } -} void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->name); + buffer.encode_string(1, this->name_ref_); 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); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->type)); +void ListEntitiesServicesArgument::calculate_size(ProtoSize &size) const { + size.add_length(1, this->name_ref_.size()); + size.add_uint32(1, static_cast(this->type)); } void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->name); + buffer.encode_string(1, this->name_ref_); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { buffer.encode_message(3, it, true); } } -void ListEntitiesServicesResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_repeated_message(total_size, 1, this->args); +void ListEntitiesServicesResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->name_ref_.size()); + size.add_fixed32(1, this->key); + size.add_repeated_message(1, this->args); } bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->bool_ = value.as_bool(); - return true; - } - case 2: { + break; + case 2: this->legacy_int = value.as_int32(); - return true; - } - case 5: { + break; + case 5: this->int_ = value.as_sint32(); - return true; - } - case 6: { + break; + case 6: this->bool_array.push_back(value.as_bool()); - return true; - } - case 7: { + break; + case 7: this->int_array.push_back(value.as_sint32()); - return true; - } + break; default: return false; } + return true; } bool ExecuteServiceArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: { + case 4: this->string_ = value.as_string(); - return true; - } - case 9: { + break; + case 9: this->string_array.push_back(value.as_string()); - return true; - } + break; default: return false; } + return true; } bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 3: { + case 3: this->float_ = value.as_float(); - return true; - } - case 8: { + break; + case 8: this->float_array.push_back(value.as_float()); - return true; - } + break; default: return false; } -} -void ExecuteServiceArgument::encode(ProtoWriteBuffer buffer) const { - buffer.encode_bool(1, this->bool_); - buffer.encode_int32(2, this->legacy_int); - buffer.encode_float(3, this->float_); - buffer.encode_string(4, this->string_); - buffer.encode_sint32(5, this->int_); - for (auto it : this->bool_array) { - buffer.encode_bool(6, it, true); - } - for (auto &it : this->int_array) { - buffer.encode_sint32(7, it, true); - } - for (auto &it : this->float_array) { - buffer.encode_float(8, it, true); - } - for (auto &it : this->string_array) { - buffer.encode_string(9, it, true); - } -} -void ExecuteServiceArgument::calculate_size(uint32_t &total_size) const { - ProtoSize::add_bool_field(total_size, 1, this->bool_); - ProtoSize::add_int32_field(total_size, 1, this->legacy_int); - ProtoSize::add_fixed_field<4>(total_size, 1, this->float_ != 0.0f); - ProtoSize::add_string_field(total_size, 1, this->string_); - ProtoSize::add_sint32_field(total_size, 1, this->int_); - if (!this->bool_array.empty()) { - for (const auto it : this->bool_array) { - ProtoSize::add_bool_field_repeated(total_size, 1, it); - } - } - if (!this->int_array.empty()) { - for (const auto &it : this->int_array) { - ProtoSize::add_sint32_field_repeated(total_size, 1, it); - } - } - if (!this->float_array.empty()) { - total_size += this->float_array.size() * 5; - } - if (!this->string_array.empty()) { - for (const auto &it : this->string_array) { - ProtoSize::add_string_field_repeated(total_size, 1, it); - } - } + return true; } bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->args.emplace_back(); value.decode_to_message(this->args.back()); - return true; - } + break; default: return false; } + return true; } bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_CAMERA void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); + buffer.encode_string(3, this->name_ref_); buffer.encode_bool(5, this->disabled_by_default); - buffer.encode_string(6, this->icon); +#ifdef USE_ENTITY_ICON + buffer.encode_string(6, this->icon_ref_); +#endif buffer.encode_uint32(7, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(8, this->device_id); +#endif } -void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); - ProtoSize::add_string_field(total_size, 1, this->icon); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->entity_category)); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void ListEntitiesCameraResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); + size.add_bool(1, this->disabled_by_default); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_uint32(1, static_cast(this->entity_category)); +#ifdef USE_DEVICES + size.add_uint32(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_bytes(2, this->data_ptr_, this->data_len_); buffer.encode_bool(3, this->done); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } -void CameraImageResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->data); - ProtoSize::add_bool_field(total_size, 1, this->done); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void CameraImageResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_length(1, this->data_len_); + size.add_bool(1, this->done); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->single = value.as_bool(); - return true; - } - case 2: { + break; + case 2: this->stream = value.as_bool(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_CLIMATE void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); + buffer.encode_string(3, this->name_ref_); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); - for (auto &it : this->supported_modes) { + for (const auto &it : *this->supported_modes) { 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) { + for (const auto &it : *this->supported_fan_modes) { buffer.encode_uint32(13, static_cast(it), true); } - for (auto &it : this->supported_swing_modes) { + for (const auto &it : *this->supported_swing_modes) { buffer.encode_uint32(14, static_cast(it), true); } - for (auto &it : this->supported_custom_fan_modes) { + for (const auto &it : *this->supported_custom_fan_modes) { buffer.encode_string(15, it, true); } - for (auto &it : this->supported_presets) { + for (const auto &it : *this->supported_presets) { buffer.encode_uint32(16, static_cast(it), true); } - for (auto &it : this->supported_custom_presets) { + for (const auto &it : *this->supported_custom_presets) { buffer.encode_string(17, it, true); } buffer.encode_bool(18, this->disabled_by_default); - buffer.encode_string(19, this->icon); +#ifdef USE_ENTITY_ICON + buffer.encode_string(19, this->icon_ref_); +#endif buffer.encode_uint32(20, static_cast(this->entity_category)); buffer.encode_float(21, this->visual_current_temperature_step); buffer.encode_bool(22, this->supports_current_humidity); buffer.encode_bool(23, this->supports_target_humidity); buffer.encode_float(24, this->visual_min_humidity); buffer.encode_float(25, this->visual_max_humidity); +#ifdef USE_DEVICES buffer.encode_uint32(26, this->device_id); +#endif } -void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_bool_field(total_size, 1, this->supports_current_temperature); - ProtoSize::add_bool_field(total_size, 1, this->supports_two_point_target_temperature); - if (!this->supported_modes.empty()) { - for (const auto &it : this->supported_modes) { - ProtoSize::add_enum_field_repeated(total_size, 1, static_cast(it)); +void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); + size.add_bool(1, this->supports_current_temperature); + size.add_bool(1, this->supports_two_point_target_temperature); + if (!this->supported_modes->empty()) { + for (const auto &it : *this->supported_modes) { + size.add_uint32_force(1, static_cast(it)); } } - ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_min_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_max_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->visual_target_temperature_step != 0.0f); - ProtoSize::add_bool_field(total_size, 1, this->legacy_supports_away); - ProtoSize::add_bool_field(total_size, 1, this->supports_action); - if (!this->supported_fan_modes.empty()) { - for (const auto &it : this->supported_fan_modes) { - ProtoSize::add_enum_field_repeated(total_size, 1, static_cast(it)); + size.add_float(1, this->visual_min_temperature); + size.add_float(1, this->visual_max_temperature); + size.add_float(1, this->visual_target_temperature_step); + size.add_bool(1, this->supports_action); + if (!this->supported_fan_modes->empty()) { + for (const auto &it : *this->supported_fan_modes) { + size.add_uint32_force(1, static_cast(it)); } } - if (!this->supported_swing_modes.empty()) { - for (const auto &it : this->supported_swing_modes) { - 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) { + size.add_uint32_force(1, static_cast(it)); } } - if (!this->supported_custom_fan_modes.empty()) { - for (const auto &it : this->supported_custom_fan_modes) { - ProtoSize::add_string_field_repeated(total_size, 1, it); + if (!this->supported_custom_fan_modes->empty()) { + for (const auto &it : *this->supported_custom_fan_modes) { + size.add_length_force(1, it.size()); } } - if (!this->supported_presets.empty()) { - for (const auto &it : this->supported_presets) { - ProtoSize::add_enum_field_repeated(total_size, 2, static_cast(it)); + if (!this->supported_presets->empty()) { + for (const auto &it : *this->supported_presets) { + size.add_uint32_force(2, static_cast(it)); } } - if (!this->supported_custom_presets.empty()) { - for (const auto &it : this->supported_custom_presets) { - ProtoSize::add_string_field_repeated(total_size, 2, it); + if (!this->supported_custom_presets->empty()) { + for (const auto &it : *this->supported_custom_presets) { + size.add_length_force(2, it.size()); } } - ProtoSize::add_bool_field(total_size, 2, this->disabled_by_default); - ProtoSize::add_string_field(total_size, 2, this->icon); - ProtoSize::add_enum_field(total_size, 2, static_cast(this->entity_category)); - ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_current_temperature_step != 0.0f); - ProtoSize::add_bool_field(total_size, 2, this->supports_current_humidity); - ProtoSize::add_bool_field(total_size, 2, this->supports_target_humidity); - ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_min_humidity != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_max_humidity != 0.0f); - ProtoSize::add_uint32_field(total_size, 2, this->device_id); + size.add_bool(2, this->disabled_by_default); +#ifdef USE_ENTITY_ICON + size.add_length(2, this->icon_ref_.size()); +#endif + size.add_uint32(2, static_cast(this->entity_category)); + size.add_float(2, this->visual_current_temperature_step); + size.add_bool(2, this->supports_current_humidity); + size.add_bool(2, this->supports_target_humidity); + size.add_float(2, this->visual_min_humidity); + size.add_float(2, this->visual_max_humidity); +#ifdef USE_DEVICES + size.add_uint32(2, this->device_id); +#endif } void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -1224,299 +1165,300 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { 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_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_string(11, this->custom_fan_mode_ref_); buffer.encode_uint32(12, static_cast(this->preset)); - buffer.encode_string(13, this->custom_preset); + buffer.encode_string(13, this->custom_preset_ref_); buffer.encode_float(14, this->current_humidity); buffer.encode_float(15, this->target_humidity); +#ifdef USE_DEVICES buffer.encode_uint32(16, this->device_id); +#endif } -void ClimateStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode)); - ProtoSize::add_fixed_field<4>(total_size, 1, this->current_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_low != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_temperature_high != 0.0f); - ProtoSize::add_bool_field(total_size, 1, this->unused_legacy_away); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->action)); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->fan_mode)); - 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_fixed_field<4>(total_size, 1, this->current_humidity != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->target_humidity != 0.0f); - ProtoSize::add_uint32_field(total_size, 2, this->device_id); +void ClimateStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_uint32(1, static_cast(this->mode)); + size.add_float(1, this->current_temperature); + size.add_float(1, this->target_temperature); + size.add_float(1, this->target_temperature_low); + size.add_float(1, this->target_temperature_high); + size.add_uint32(1, static_cast(this->action)); + size.add_uint32(1, static_cast(this->fan_mode)); + size.add_uint32(1, static_cast(this->swing_mode)); + size.add_length(1, this->custom_fan_mode_ref_.size()); + size.add_uint32(1, static_cast(this->preset)); + size.add_length(1, this->custom_preset_ref_.size()); + size.add_float(1, this->current_humidity); + size.add_float(1, this->target_humidity); +#ifdef USE_DEVICES + size.add_uint32(2, this->device_id); +#endif } bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_mode = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->mode = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; + case 4: this->has_target_temperature = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_target_temperature_low = value.as_bool(); - return true; - } - case 8: { + break; + case 8: this->has_target_temperature_high = value.as_bool(); - return true; - } - case 10: { - 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: { + break; + case 13: this->fan_mode = static_cast(value.as_uint32()); - return true; - } - case 14: { + break; + case 14: this->has_swing_mode = value.as_bool(); - return true; - } - case 15: { + break; + case 15: this->swing_mode = static_cast(value.as_uint32()); - return true; - } - case 16: { + break; + case 16: this->has_custom_fan_mode = value.as_bool(); - return true; - } - case 18: { + break; + case 18: this->has_preset = value.as_bool(); - return true; - } - case 19: { + break; + case 19: this->preset = static_cast(value.as_uint32()); - return true; - } - case 20: { + break; + case 20: this->has_custom_preset = value.as_bool(); - return true; - } - case 22: { + break; + case 22: this->has_target_humidity = value.as_bool(); - return true; - } - case 24: { + break; +#ifdef USE_DEVICES + case 24: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 17: { + case 17: this->custom_fan_mode = value.as_string(); - return true; - } - case 21: { + break; + case 21: this->custom_preset = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 5: { + break; + case 5: this->target_temperature = value.as_float(); - return true; - } - case 7: { + break; + case 7: this->target_temperature_low = value.as_float(); - return true; - } - case 9: { + break; + case 9: this->target_temperature_high = value.as_float(); - return true; - } - case 23: { + break; + case 23: this->target_humidity = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_NUMBER void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#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_uint32(10, static_cast(this->entity_category)); - buffer.encode_string(11, this->unit_of_measurement); + buffer.encode_string(11, this->unit_of_measurement_ref_); buffer.encode_uint32(12, static_cast(this->mode)); - buffer.encode_string(13, this->device_class); + buffer.encode_string(13, this->device_class_ref_); +#ifdef USE_DEVICES buffer.encode_uint32(14, this->device_id); +#endif } -void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - ProtoSize::add_fixed_field<4>(total_size, 1, this->min_value != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->max_value != 0.0f); - ProtoSize::add_fixed_field<4>(total_size, 1, this->step != 0.0f); - 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); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void ListEntitiesNumberResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_float(1, this->min_value); + size.add_float(1, this->max_value); + size.add_float(1, this->step); + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); + size.add_length(1, this->unit_of_measurement_ref_.size()); + size.add_uint32(1, static_cast(this->mode)); + size.add_length(1, this->device_class_ref_.size()); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } -void NumberStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_fixed_field<4>(total_size, 1, this->state != 0.0f); - ProtoSize::add_bool_field(total_size, 1, this->missing_state); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void NumberStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_float(1, this->state); + size.add_bool(1, this->missing_state); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool NumberCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 3: { +#ifdef USE_DEVICES + case 3: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 2: { + break; + case 2: this->state = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_SELECT void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); - for (auto &it : this->options) { + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif + for (const auto &it : *this->options) { buffer.encode_string(6, it, true); } buffer.encode_bool(7, this->disabled_by_default); buffer.encode_uint32(8, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); +#endif } -void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - if (!this->options.empty()) { - for (const auto &it : this->options) { - ProtoSize::add_string_field_repeated(total_size, 1, it); +void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + if (!this->options->empty()) { + for (const auto &it : *this->options) { + size.add_length_force(1, it.size()); } } - 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->device_id); + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); +#ifdef USE_DEVICES + size.add_uint32(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_string(2, this->state_ref_); buffer.encode_bool(3, this->missing_state); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } -void SelectStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->state); - ProtoSize::add_bool_field(total_size, 1, this->missing_state); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void SelectStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_length(1, this->state_ref_.size()); + size.add_bool(1, this->missing_state); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 3: { +#ifdef USE_DEVICES + case 3: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->state = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_SIREN void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); for (auto &it : this->tones) { buffer.encode_string(7, it, true); @@ -1524,510 +1466,410 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(8, this->supports_duration); buffer.encode_bool(9, this->supports_volume); buffer.encode_uint32(10, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(11, this->device_id); +#endif } -void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default); +void ListEntitiesSirenResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); if (!this->tones.empty()) { for (const auto &it : this->tones) { - ProtoSize::add_string_field_repeated(total_size, 1, it); + size.add_length_force(1, it.size()); } } - 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)); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); + size.add_bool(1, this->supports_duration); + size.add_bool(1, this->supports_volume); + size.add_uint32(1, static_cast(this->entity_category)); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void SirenStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); +#ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); +#endif } -void SirenStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_bool_field(total_size, 1, this->state); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void SirenStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_bool(1, this->state); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_state = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->state = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->has_tone = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_duration = value.as_bool(); - return true; - } - case 7: { + break; + case 7: this->duration = value.as_uint32(); - return true; - } - case 8: { + break; + case 8: this->has_volume = value.as_bool(); - return true; - } - case 10: { + break; +#ifdef USE_DEVICES + case 10: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool SirenCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 5: { + case 5: this->tone = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 9: { + break; + case 9: this->volume = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_LOCK void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_bool(8, this->assumed_state); buffer.encode_bool(9, this->supports_open); buffer.encode_bool(10, this->requires_code); - buffer.encode_string(11, this->code_format); + buffer.encode_string(11, this->code_format_ref_); +#ifdef USE_DEVICES buffer.encode_uint32(12, this->device_id); +#endif } -void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void ListEntitiesLockResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); + size.add_bool(1, this->assumed_state); + size.add_bool(1, this->supports_open); + size.add_bool(1, this->requires_code); + size.add_length(1, this->code_format_ref_.size()); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void LockStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); +#ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); +#endif } -void LockStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->state)); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void LockStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_uint32(1, static_cast(this->state)); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->command = static_cast(value.as_uint32()); - return true; - } - case 3: { + break; + case 3: this->has_code = value.as_bool(); - return true; - } - case 5: { + break; +#ifdef USE_DEVICES + case 5: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool LockCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: { + case 4: this->code = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_BUTTON void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + buffer.encode_string(8, this->device_class_ref_); +#ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); +#endif } -void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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_uint32_field(total_size, 1, this->device_id); +void ListEntitiesButtonResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); + size.add_length(1, this->device_class_ref_.size()); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool ButtonCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { +#ifdef USE_DEVICES + case 2: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_MEDIA_PLAYER -bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->sample_rate = value.as_uint32(); - return true; - } - case 3: { - this->num_channels = value.as_uint32(); - return true; - } - case 4: { - this->purpose = static_cast(value.as_uint32()); - return true; - } - case 5: { - this->sample_bytes = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool MediaPlayerSupportedFormat::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->format = value.as_string(); - return true; - } - default: - return false; - } -} void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->format); + buffer.encode_string(1, this->format_ref_); buffer.encode_uint32(2, this->sample_rate); buffer.encode_uint32(3, this->num_channels); 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); - 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 MediaPlayerSupportedFormat::calculate_size(ProtoSize &size) const { + size.add_length(1, this->format_ref_.size()); + size.add_uint32(1, this->sample_rate); + size.add_uint32(1, this->num_channels); + size.add_uint32(1, static_cast(this->purpose)); + size.add_uint32(1, this->sample_bytes); } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_bool(8, this->supports_pause); for (auto &it : this->supported_formats) { buffer.encode_message(9, it, true); } +#ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); +#endif + buffer.encode_uint32(11, this->feature_flags); } -void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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); +void ListEntitiesMediaPlayerResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); + size.add_bool(1, this->supports_pause); + size.add_repeated_message(1, this->supported_formats); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif + size.add_uint32(1, this->feature_flags); } void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); buffer.encode_float(3, this->volume); buffer.encode_bool(4, this->muted); +#ifdef USE_DEVICES buffer.encode_uint32(5, this->device_id); +#endif } -void MediaPlayerStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->state)); - ProtoSize::add_fixed_field<4>(total_size, 1, this->volume != 0.0f); - ProtoSize::add_bool_field(total_size, 1, this->muted); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void MediaPlayerStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_uint32(1, static_cast(this->state)); + size.add_float(1, this->volume); + size.add_bool(1, this->muted); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_command = value.as_bool(); - return true; - } - case 3: { + break; + case 3: this->command = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; + case 4: this->has_volume = value.as_bool(); - return true; - } - case 6: { + break; + case 6: this->has_media_url = value.as_bool(); - return true; - } - case 8: { + break; + case 8: this->has_announcement = value.as_bool(); - return true; - } - case 9: { + break; + case 9: this->announcement = value.as_bool(); - return true; - } - case 10: { + break; +#ifdef USE_DEVICES + case 10: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 7: { + case 7: this->media_url = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 5: { + break; + case 5: this->volume = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_BLUETOOTH_PROXY bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->flags = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 2: { - this->legacy_data.push_back(value.as_uint32()); - return true; - } - default: - return false; - } -} -bool BluetoothServiceData::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->uuid = value.as_string(); - return true; - } - case 3: { - this->data = value.as_string(); - return true; - } - 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); - if (!this->legacy_data.empty()) { - for (const auto &it : this->legacy_data) { - ProtoSize::add_uint32_field_repeated(total_size, 1, it); - } - } - ProtoSize::add_string_field(total_size, 1, this->data); -} -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); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_sint32_field(total_size, 1, this->rssi); - if (!this->service_uuids.empty()) { - for (const auto &it : this->service_uuids) { - ProtoSize::add_string_field_repeated(total_size, 1, it); - } - } - 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); -} -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); - ProtoSize::add_sint32_field(total_size, 1, this->rssi); - ProtoSize::add_uint32_field(total_size, 1, this->address_type); - ProtoSize::add_string_field(total_size, 1, this->data); +void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { + size.add_uint64(1, this->address); + size.add_sint32(1, this->rssi); + size.add_uint32(1, this->address_type); + size.add_length(1, this->data_len); } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { - for (auto &it : this->advertisements) { - buffer.encode_message(1, it, true); + for (uint16_t i = 0; i < this->advertisements_len; i++) { + buffer.encode_message(1, this->advertisements[i], true); } } -void BluetoothLERawAdvertisementsResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_repeated_message(total_size, 1, this->advertisements); +void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { + for (uint16_t i = 0; i < this->advertisements_len; i++) { + size.add_message_object_force(1, this->advertisements[i]); + } } bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->request_type = static_cast(value.as_uint32()); - return true; - } - case 3: { + break; + case 3: this->has_address_type = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->address_type = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -2035,141 +1877,79 @@ void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(3, this->mtu); buffer.encode_int32(4, this->error); } -void BluetoothDeviceConnectionResponse::calculate_size(uint32_t &total_size) const { - 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); +void BluetoothDeviceConnectionResponse::calculate_size(ProtoSize &size) const { + size.add_uint64(1, this->address); + size.add_bool(1, this->connected); + size.add_uint32(1, this->mtu); + size.add_int32(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; - } -} -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) { - buffer.encode_uint64(1, it, true); + if (this->uuid[0] != 0 || this->uuid[1] != 0) { + buffer.encode_uint64(1, this->uuid[0], true); + buffer.encode_uint64(1, this->uuid[1], true); } buffer.encode_uint32(2, this->handle); + buffer.encode_uint32(3, this->short_uuid); } -void BluetoothGATTDescriptor::calculate_size(uint32_t &total_size) const { - if (!this->uuid.empty()) { - for (const auto &it : this->uuid) { - ProtoSize::add_uint64_field_repeated(total_size, 1, it); - } - } - ProtoSize::add_uint32_field(total_size, 1, this->handle); -} -bool BluetoothGATTCharacteristic::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->uuid.push_back(value.as_uint64()); - return true; - } - case 2: { - this->handle = value.as_uint32(); - return true; - } - case 3: { - this->properties = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool BluetoothGATTCharacteristic::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 4: { - this->descriptors.emplace_back(); - value.decode_to_message(this->descriptors.back()); - return true; - } - default: - return false; +void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const { + if (this->uuid[0] != 0 || this->uuid[1] != 0) { + size.add_uint64_force(1, this->uuid[0]); + size.add_uint64_force(1, this->uuid[1]); } + size.add_uint32(1, this->handle); + size.add_uint32(1, this->short_uuid); } void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { - for (auto &it : this->uuid) { - buffer.encode_uint64(1, it, true); + if (this->uuid[0] != 0 || this->uuid[1] != 0) { + buffer.encode_uint64(1, this->uuid[0], true); + buffer.encode_uint64(1, this->uuid[1], true); } 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_uint32(5, this->short_uuid); } -void BluetoothGATTCharacteristic::calculate_size(uint32_t &total_size) const { - if (!this->uuid.empty()) { - for (const auto &it : this->uuid) { - ProtoSize::add_uint64_field_repeated(total_size, 1, it); - } - } - 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.emplace_back(); - value.decode_to_message(this->characteristics.back()); - return true; - } - default: - return false; +void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const { + if (this->uuid[0] != 0 || this->uuid[1] != 0) { + size.add_uint64_force(1, this->uuid[0]); + size.add_uint64_force(1, this->uuid[1]); } + size.add_uint32(1, this->handle); + size.add_uint32(1, this->properties); + size.add_repeated_message(1, this->descriptors); + size.add_uint32(1, this->short_uuid); } void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { - for (auto &it : this->uuid) { - buffer.encode_uint64(1, it, true); + if (this->uuid[0] != 0 || this->uuid[1] != 0) { + buffer.encode_uint64(1, this->uuid[0], true); + buffer.encode_uint64(1, this->uuid[1], true); } buffer.encode_uint32(2, this->handle); for (auto &it : this->characteristics) { buffer.encode_message(3, it, true); } + buffer.encode_uint32(4, this->short_uuid); } -void BluetoothGATTService::calculate_size(uint32_t &total_size) const { - if (!this->uuid.empty()) { - for (const auto &it : this->uuid) { - ProtoSize::add_uint64_field_repeated(total_size, 1, it); - } +void BluetoothGATTService::calculate_size(ProtoSize &size) const { + if (this->uuid[0] != 0 || this->uuid[1] != 0) { + size.add_uint64_force(1, this->uuid[0]); + size.add_uint64_force(1, this->uuid[1]); } - ProtoSize::add_uint32_field(total_size, 1, this->handle); - ProtoSize::add_repeated_message(total_size, 1, this->characteristics); + size.add_uint32(1, this->handle); + size.add_repeated_message(1, this->characteristics); + size.add_uint32(1, this->short_uuid); } void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); @@ -2177,147 +1957,140 @@ void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_message(2, it, true); } } -void BluetoothGATTGetServicesResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_uint64_field(total_size, 1, this->address); - ProtoSize::add_repeated_message(total_size, 1, this->services); +void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { + size.add_uint64(1, this->address); + size.add_repeated_message(1, this->services); } 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); -} +void BluetoothGATTGetServicesDoneResponse::calculate_size(ProtoSize &size) const { size.add_uint64(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; - } + break; default: return false; } + return true; } void BluetoothGATTReadResponse::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()); + buffer.encode_bytes(3, this->data_ptr_, this->data_len_); } -void BluetoothGATTReadResponse::calculate_size(uint32_t &total_size) const { - 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 BluetoothGATTReadResponse::calculate_size(ProtoSize &size) const { + size.add_uint64(1, this->address); + size.add_uint32(1, this->handle); + size.add_length(1, this->data_len_); } bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->response = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: { + case 4: this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTReadDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 3: { + case 3: this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->address = value.as_uint64(); - return true; - } - case 2: { + break; + case 2: this->handle = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->enable = value.as_bool(); - return true; - } + break; default: return false; } + return true; } void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); - buffer.encode_bytes(3, reinterpret_cast(this->data.data()), this->data.size()); + buffer.encode_bytes(3, this->data_ptr_, this->data_len_); } -void BluetoothGATTNotifyDataResponse::calculate_size(uint32_t &total_size) const { - 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 BluetoothGATTNotifyDataResponse::calculate_size(ProtoSize &size) const { + size.add_uint64(1, this->address); + size.add_uint32(1, this->handle); + size.add_length(1, this->data_len_); } void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->free); buffer.encode_uint32(2, this->limit); - for (auto &it : this->allocated) { - buffer.encode_uint64(3, it, true); + for (const auto &it : this->allocated) { + if (it != 0) { + buffer.encode_uint64(3, it, true); + } } } -void BluetoothConnectionsFreeResponse::calculate_size(uint32_t &total_size) const { - 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_repeated(total_size, 1, it); +void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const { + size.add_uint32(1, this->free); + size.add_uint32(1, this->limit); + for (const auto &it : this->allocated) { + if (it != 0) { + size.add_uint64_force(1, it); } } } @@ -2326,323 +2099,262 @@ void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer buffer) const { 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); - ProtoSize::add_uint32_field(total_size, 1, this->handle); - ProtoSize::add_int32_field(total_size, 1, this->error); +void BluetoothGATTErrorResponse::calculate_size(ProtoSize &size) const { + size.add_uint64(1, this->address); + size.add_uint32(1, this->handle); + size.add_int32(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); - ProtoSize::add_uint32_field(total_size, 1, this->handle); +void BluetoothGATTWriteResponse::calculate_size(ProtoSize &size) const { + size.add_uint64(1, this->address); + size.add_uint32(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); - ProtoSize::add_uint32_field(total_size, 1, this->handle); +void BluetoothGATTNotifyResponse::calculate_size(ProtoSize &size) const { + size.add_uint64(1, this->address); + size.add_uint32(1, this->handle); } void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->paired); buffer.encode_int32(3, this->error); } -void BluetoothDevicePairingResponse::calculate_size(uint32_t &total_size) const { - 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 BluetoothDevicePairingResponse::calculate_size(ProtoSize &size) const { + size.add_uint64(1, this->address); + size.add_bool(1, this->paired); + size.add_int32(1, this->error); } void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); } -void BluetoothDeviceUnpairingResponse::calculate_size(uint32_t &total_size) const { - 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 BluetoothDeviceUnpairingResponse::calculate_size(ProtoSize &size) const { + size.add_uint64(1, this->address); + size.add_bool(1, this->success); + size.add_int32(1, this->error); } void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); } -void BluetoothDeviceClearCacheResponse::calculate_size(uint32_t &total_size) const { - 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::calculate_size(ProtoSize &size) const { + size.add_uint64(1, this->address); + size.add_bool(1, this->success); + size.add_int32(1, this->error); } void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { 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)); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->mode)); +void BluetoothScannerStateResponse::calculate_size(ProtoSize &size) const { + size.add_uint32(1, static_cast(this->state)); + size.add_uint32(1, static_cast(this->mode)); } bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->mode = static_cast(value.as_uint32()); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_VOICE_ASSISTANT bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->subscribe = value.as_bool(); - return true; - } - case 2: { + break; + case 2: this->flags = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool VoiceAssistantAudioSettings::decode_varint(uint32_t field_id, ProtoVarInt value) { - switch (field_id) { - case 1: { - this->noise_suppression_level = value.as_uint32(); - return true; - } - case 2: { - this->auto_gain = value.as_uint32(); - return true; - } - default: - return false; - } -} -bool VoiceAssistantAudioSettings::decode_32bit(uint32_t field_id, Proto32Bit value) { - switch (field_id) { - case 3: { - this->volume_multiplier = value.as_float(); - return true; - } + break; default: return false; } + return true; } void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->noise_suppression_level); buffer.encode_uint32(2, this->auto_gain); 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); - ProtoSize::add_uint32_field(total_size, 1, this->auto_gain); - ProtoSize::add_fixed_field<4>(total_size, 1, this->volume_multiplier != 0.0f); +void VoiceAssistantAudioSettings::calculate_size(ProtoSize &size) const { + size.add_uint32(1, this->noise_suppression_level); + size.add_uint32(1, this->auto_gain); + size.add_float(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_string(2, this->conversation_id_ref_); buffer.encode_uint32(3, this->flags); buffer.encode_message(4, this->audio_settings); - buffer.encode_string(5, this->wake_word_phrase); + buffer.encode_string(5, this->wake_word_phrase_ref_); } -void VoiceAssistantRequest::calculate_size(uint32_t &total_size) const { - 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); +void VoiceAssistantRequest::calculate_size(ProtoSize &size) const { + size.add_bool(1, this->start); + size.add_length(1, this->conversation_id_ref_.size()); + size.add_uint32(1, this->flags); + size.add_message_object(1, this->audio_settings); + size.add_length(1, this->wake_word_phrase_ref_.size()); } bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->port = value.as_uint32(); - return true; - } - case 2: { + break; + case 2: this->error = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantEventData::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->name = value.as_string(); - return true; - } - case 2: { + break; + case 2: this->value = value.as_string(); - return true; - } + break; default: return false; } -} -void VoiceAssistantEventData::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->name); - buffer.encode_string(2, this->value); -} -void VoiceAssistantEventData::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->value); + return true; } bool VoiceAssistantEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->event_type = static_cast(value.as_uint32()); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->data.emplace_back(); value.decode_to_message(this->data.back()); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAudio::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->end = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->data = value.as_string(); - return true; - } + break; default: return false; } + return true; } void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { - buffer.encode_bytes(1, reinterpret_cast(this->data.data()), this->data.size()); + buffer.encode_bytes(1, this->data_ptr_, this->data_len_); buffer.encode_bool(2, this->end); } -void VoiceAssistantAudio::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->data); - ProtoSize::add_bool_field(total_size, 1, this->end); +void VoiceAssistantAudio::calculate_size(ProtoSize &size) const { + size.add_length(1, this->data_len_); + size.add_bool(1, this->end); } bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 1: { + case 1: this->event_type = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; + case 4: this->total_seconds = value.as_uint32(); - return true; - } - case 5: { + break; + case 5: this->seconds_left = value.as_uint32(); - return true; - } - case 6: { + break; + case 6: this->is_active = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantTimerEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->timer_id = value.as_string(); - return true; - } - case 3: { + break; + case 3: this->name = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 4: { + case 4: this->start_conversation = value.as_bool(); - return true; - } + break; default: return false; } + return true; } bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: { + case 1: this->media_id = value.as_string(); - return true; - } - case 2: { + break; + case 2: this->text = value.as_string(); - return true; - } - case 3: { + break; + case 3: this->preannounce_media_id = value.as_string(); - return true; - } + break; default: return false; } + return true; } void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } -void VoiceAssistantAnnounceFinished::calculate_size(uint32_t &total_size) const { - ProtoSize::add_bool_field(total_size, 1, this->success); -} -bool VoiceAssistantWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - this->id = value.as_string(); - return true; - } - case 2: { - this->wake_word = value.as_string(); - return true; - } - case 3: { - this->trained_languages.push_back(value.as_string()); - return true; - } - default: - return false; - } -} +void VoiceAssistantAnnounceFinished::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->id); - buffer.encode_string(2, this->wake_word); + buffer.encode_string(1, this->id_ref_); + buffer.encode_string(2, this->wake_word_ref_); for (auto &it : this->trained_languages) { buffer.encode_string(3, it, true); } } -void VoiceAssistantWakeWord::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->id); - ProtoSize::add_string_field(total_size, 1, this->wake_word); +void VoiceAssistantWakeWord::calculate_size(ProtoSize &size) const { + size.add_length(1, this->id_ref_.size()); + size.add_length(1, this->wake_word_ref_.size()); if (!this->trained_languages.empty()) { for (const auto &it : this->trained_languages) { - ProtoSize::add_string_field_repeated(total_size, 1, it); + size.add_length_force(1, it.size()); } } } @@ -2650,195 +2362,224 @@ void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const for (auto &it : this->available_wake_words) { buffer.encode_message(1, it, true); } - for (auto &it : this->active_wake_words) { + for (const auto &it : *this->active_wake_words) { buffer.encode_string(2, it, true); } buffer.encode_uint32(3, this->max_active_wake_words); } -void VoiceAssistantConfigurationResponse::calculate_size(uint32_t &total_size) const { - 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_repeated(total_size, 1, it); +void VoiceAssistantConfigurationResponse::calculate_size(ProtoSize &size) const { + size.add_repeated_message(1, this->available_wake_words); + if (!this->active_wake_words->empty()) { + for (const auto &it : *this->active_wake_words) { + size.add_length_force(1, it.size()); } } - ProtoSize::add_uint32_field(total_size, 1, this->max_active_wake_words); + size.add_uint32(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; } + return true; } #endif #ifdef USE_ALARM_CONTROL_PANEL void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_uint32(8, this->supported_features); buffer.encode_bool(9, this->requires_code); buffer.encode_bool(10, this->requires_code_to_arm); +#ifdef USE_DEVICES buffer.encode_uint32(11, this->device_id); +#endif } -void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void ListEntitiesAlarmControlPanelResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); + size.add_uint32(1, this->supported_features); + size.add_bool(1, this->requires_code); + size.add_bool(1, this->requires_code_to_arm); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_uint32(2, static_cast(this->state)); +#ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); +#endif } -void AlarmControlPanelStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->state)); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void AlarmControlPanelStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_uint32(1, static_cast(this->state)); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->command = static_cast(value.as_uint32()); - return true; - } - case 4: { + break; +#ifdef USE_DEVICES + case 4: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool AlarmControlPanelCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 3: { + case 3: this->code = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_TEXT void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_uint32(8, this->min_length); buffer.encode_uint32(9, this->max_length); - buffer.encode_string(10, this->pattern); + buffer.encode_string(10, this->pattern_ref_); buffer.encode_uint32(11, static_cast(this->mode)); +#ifdef USE_DEVICES buffer.encode_uint32(12, this->device_id); +#endif } -void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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)); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void ListEntitiesTextResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); + size.add_uint32(1, this->min_length); + size.add_uint32(1, this->max_length); + size.add_length(1, this->pattern_ref_.size()); + size.add_uint32(1, static_cast(this->mode)); +#ifdef USE_DEVICES + size.add_uint32(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_string(2, this->state_ref_); buffer.encode_bool(3, this->missing_state); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } -void TextStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->state); - ProtoSize::add_bool_field(total_size, 1, this->missing_state); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void TextStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_length(1, this->state_ref_.size()); + size.add_bool(1, this->missing_state); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 3: { +#ifdef USE_DEVICES + case 3: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: { + case 2: this->state = value.as_string(); - return true; - } + break; default: return false; } + return true; } bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_DATETIME_DATE void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(8, this->device_id); +#endif } -void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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->device_id); +void ListEntitiesDateResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void DateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -2846,69 +2587,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); - 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); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void DateStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_bool(1, this->missing_state); + size.add_uint32(1, this->year); + size.add_uint32(1, this->month); + size.add_uint32(1, this->day); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->year = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->month = value.as_uint32(); - return true; - } - case 4: { + break; + case 4: this->day = value.as_uint32(); - return true; - } - case 5: { + break; +#ifdef USE_DEVICES + case 5: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_DATETIME_TIME void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(8, this->device_id); +#endif } -void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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->device_id); +void ListEntitiesTimeResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -2916,245 +2666,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); - 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); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void TimeStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_bool(1, this->missing_state); + size.add_uint32(1, this->hour); + size.add_uint32(1, this->minute); + size.add_uint32(1, this->second); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->hour = value.as_uint32(); - return true; - } - case 3: { + break; + case 3: this->minute = value.as_uint32(); - return true; - } - case 4: { + break; + case 4: this->second = value.as_uint32(); - return true; - } - case 5: { + break; +#ifdef USE_DEVICES + case 5: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_EVENT void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + buffer.encode_string(8, this->device_class_ref_); for (auto &it : this->event_types) { buffer.encode_string(9, it, true); } +#ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); +#endif } -void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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); +void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); + size.add_length(1, this->device_class_ref_.size()); if (!this->event_types.empty()) { for (const auto &it : this->event_types) { - ProtoSize::add_string_field_repeated(total_size, 1, it); + size.add_length_force(1, it.size()); } } - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void EventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_string(2, this->event_type); + buffer.encode_string(2, this->event_type_ref_); +#ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); +#endif } -void EventResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->event_type); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void EventResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_length(1, this->event_type_ref_.size()); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } #endif #ifdef USE_VALVE void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + buffer.encode_string(8, this->device_class_ref_); buffer.encode_bool(9, this->assumed_state); buffer.encode_bool(10, this->supports_position); buffer.encode_bool(11, this->supports_stop); +#ifdef USE_DEVICES buffer.encode_uint32(12, this->device_id); +#endif } -void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void ListEntitiesValveResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); + size.add_length(1, this->device_class_ref_.size()); + size.add_bool(1, this->assumed_state); + size.add_bool(1, this->supports_position); + size.add_bool(1, this->supports_stop); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void ValveStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_float(2, this->position); buffer.encode_uint32(3, static_cast(this->current_operation)); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } -void ValveStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_fixed_field<4>(total_size, 1, this->position != 0.0f); - ProtoSize::add_enum_field(total_size, 1, static_cast(this->current_operation)); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void ValveStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_float(1, this->position); + size.add_uint32(1, static_cast(this->current_operation)); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->has_position = value.as_bool(); - return true; - } - case 4: { + break; + case 4: this->stop = value.as_bool(); - return true; - } - case 5: { + break; +#ifdef USE_DEVICES + case 5: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 3: { + break; + case 3: this->position = value.as_float(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_DATETIME_DATETIME void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); +#ifdef USE_DEVICES buffer.encode_uint32(8, this->device_id); +#endif } -void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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->device_id); +void ListEntitiesDateTimeResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_fixed32(3, this->epoch_seconds); +#ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); +#endif } -void DateTimeStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_bool_field(total_size, 1, this->missing_state); - ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void DateTimeStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_bool(1, this->missing_state); + size.add_fixed32(1, this->epoch_seconds); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool DateTimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 3: { +#ifdef USE_DEVICES + case 3: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } - case 2: { + break; + case 2: this->epoch_seconds = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif #ifdef USE_UPDATE void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id); + buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name); - buffer.encode_string(4, this->unique_id); - buffer.encode_string(5, this->icon); + buffer.encode_string(3, this->name_ref_); +#ifdef USE_ENTITY_ICON + buffer.encode_string(5, this->icon_ref_); +#endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + buffer.encode_string(8, this->device_class_ref_); +#ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); +#endif } -void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_string_field(total_size, 1, this->object_id); - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_string_field(total_size, 1, this->name); - ProtoSize::add_string_field(total_size, 1, this->unique_id); - ProtoSize::add_string_field(total_size, 1, this->icon); - 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_uint32_field(total_size, 1, this->device_id); +void ListEntitiesUpdateResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id_ref_.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name_ref_.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon_ref_.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); + size.add_length(1, this->device_class_ref_.size()); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); @@ -3162,51 +2951,55 @@ void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->in_progress); buffer.encode_bool(4, this->has_progress); buffer.encode_float(5, this->progress); - buffer.encode_string(6, this->current_version); - buffer.encode_string(7, this->latest_version); - buffer.encode_string(8, this->title); - buffer.encode_string(9, this->release_summary); - buffer.encode_string(10, this->release_url); + buffer.encode_string(6, this->current_version_ref_); + buffer.encode_string(7, this->latest_version_ref_); + buffer.encode_string(8, this->title_ref_); + buffer.encode_string(9, this->release_summary_ref_); + buffer.encode_string(10, this->release_url_ref_); +#ifdef USE_DEVICES buffer.encode_uint32(11, this->device_id); +#endif } -void UpdateStateResponse::calculate_size(uint32_t &total_size) const { - ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); - ProtoSize::add_bool_field(total_size, 1, this->missing_state); - ProtoSize::add_bool_field(total_size, 1, this->in_progress); - ProtoSize::add_bool_field(total_size, 1, this->has_progress); - ProtoSize::add_fixed_field<4>(total_size, 1, this->progress != 0.0f); - ProtoSize::add_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); - ProtoSize::add_uint32_field(total_size, 1, this->device_id); +void UpdateStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_bool(1, this->missing_state); + size.add_bool(1, this->in_progress); + size.add_bool(1, this->has_progress); + size.add_float(1, this->progress); + size.add_length(1, this->current_version_ref_.size()); + size.add_length(1, this->latest_version_ref_.size()); + size.add_length(1, this->title_ref_.size()); + size.add_length(1, this->release_summary_ref_.size()); + size.add_length(1, this->release_url_ref_.size()); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif } bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { - case 2: { + case 2: this->command = static_cast(value.as_uint32()); - return true; - } - case 3: { + break; +#ifdef USE_DEVICES + case 3: this->device_id = value.as_uint32(); - return true; - } + break; +#endif default: return false; } + return true; } bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { - case 1: { + case 1: this->key = value.as_fixed32(); - return true; - } + break; default: return false; } + return true; } #endif -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 3f2d4afad3..edf839be55 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -3,11 +3,12 @@ #pragma once #include "esphome/core/defines.h" +#include "esphome/core/string_ref.h" #include "proto.h" +#include "api_pb2_includes.h" -namespace esphome { -namespace api { +namespace esphome::api { namespace enums { @@ -17,27 +18,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, @@ -65,11 +52,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, @@ -168,6 +150,9 @@ enum MediaPlayerState : uint32_t { MEDIA_PLAYER_STATE_IDLE = 1, MEDIA_PLAYER_STATE_PLAYING = 2, MEDIA_PLAYER_STATE_PAUSED = 3, + MEDIA_PLAYER_STATE_ANNOUNCING = 4, + MEDIA_PLAYER_STATE_OFF = 5, + MEDIA_PLAYER_STATE_ON = 6, }; enum MediaPlayerCommand : uint32_t { MEDIA_PLAYER_COMMAND_PLAY = 0, @@ -175,6 +160,15 @@ enum MediaPlayerCommand : uint32_t { MEDIA_PLAYER_COMMAND_STOP = 2, MEDIA_PLAYER_COMMAND_MUTE = 3, MEDIA_PLAYER_COMMAND_UNMUTE = 4, + MEDIA_PLAYER_COMMAND_TOGGLE = 5, + MEDIA_PLAYER_COMMAND_VOLUME_UP = 6, + MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7, + MEDIA_PLAYER_COMMAND_ENQUEUE = 8, + MEDIA_PLAYER_COMMAND_REPEAT_ONE = 9, + MEDIA_PLAYER_COMMAND_REPEAT_OFF = 10, + MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11, + MEDIA_PLAYER_COMMAND_TURN_ON = 12, + MEDIA_PLAYER_COMMAND_TURN_OFF = 13, }; enum MediaPlayerFormatPurpose : uint32_t { MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0, @@ -288,14 +282,20 @@ enum UpdateCommand : uint32_t { class InfoResponseProtoMessage : public ProtoMessage { public: ~InfoResponseProtoMessage() override = default; - std::string object_id{}; + StringRef object_id_ref_{}; + void set_object_id(const StringRef &ref) { this->object_id_ref_ = ref; } uint32_t key{0}; - std::string name{}; - std::string unique_id{}; + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } bool disabled_by_default{false}; - std::string icon{}; +#ifdef USE_ENTITY_ICON + StringRef icon_ref_{}; + void set_icon(const StringRef &ref) { this->icon_ref_ = ref; } +#endif enums::EntityCategory entity_category{}; +#ifdef USE_DEVICES uint32_t device_id{0}; +#endif protected: }; @@ -304,20 +304,24 @@ class StateResponseProtoMessage : public ProtoMessage { public: ~StateResponseProtoMessage() override = default; uint32_t key{0}; +#ifdef USE_DEVICES uint32_t device_id{0}; +#endif protected: }; -class CommandProtoMessage : public ProtoMessage { +class CommandProtoMessage : public ProtoDecodableMessage { public: ~CommandProtoMessage() override = default; uint32_t key{0}; +#ifdef USE_DEVICES uint32_t device_id{0}; +#endif protected: }; -class HelloRequest : public ProtoMessage { +class HelloRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 1; static constexpr uint8_t ESTIMATED_SIZE = 17; @@ -344,17 +348,19 @@ class HelloResponse : public ProtoMessage { #endif uint32_t api_version_major{0}; uint32_t api_version_minor{0}; - std::string server_info{}; - std::string name{}; + StringRef server_info_ref_{}; + void set_server_info(const StringRef &ref) { this->server_info_ref_ = ref; } + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; -class ConnectRequest : public ProtoMessage { +class ConnectRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 3; static constexpr uint8_t ESTIMATED_SIZE = 9; @@ -378,7 +384,7 @@ class ConnectResponse : public ProtoMessage { #endif bool invalid_password{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -450,66 +456,103 @@ class DeviceInfoRequest : public ProtoMessage { protected: }; +#ifdef USE_AREAS class AreaInfo : public ProtoMessage { public: uint32_t area_id{0}; - std::string name{}; + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif +#ifdef USE_DEVICES class DeviceInfo : public ProtoMessage { public: uint32_t device_id{0}; - std::string name{}; + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } uint32_t area_id{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +#endif class DeviceInfoResponse : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint8_t ESTIMATED_SIZE = 219; + static constexpr uint8_t ESTIMATED_SIZE = 247; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif +#ifdef USE_API_PASSWORD bool uses_password{false}; - std::string name{}; - std::string mac_address{}; - std::string esphome_version{}; - std::string compilation_time{}; - std::string model{}; +#endif + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } + StringRef mac_address_ref_{}; + void set_mac_address(const StringRef &ref) { this->mac_address_ref_ = ref; } + StringRef esphome_version_ref_{}; + void set_esphome_version(const StringRef &ref) { this->esphome_version_ref_ = ref; } + StringRef compilation_time_ref_{}; + void set_compilation_time(const StringRef &ref) { this->compilation_time_ref_ = ref; } + StringRef model_ref_{}; + void set_model(const StringRef &ref) { this->model_ref_ = ref; } +#ifdef USE_DEEP_SLEEP bool has_deep_sleep{false}; - std::string project_name{}; - std::string project_version{}; +#endif +#ifdef ESPHOME_PROJECT_NAME + StringRef project_name_ref_{}; + void set_project_name(const StringRef &ref) { this->project_name_ref_ = ref; } +#endif +#ifdef ESPHOME_PROJECT_NAME + StringRef project_version_ref_{}; + void set_project_version(const StringRef &ref) { this->project_version_ref_ = ref; } +#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}; - std::string manufacturer{}; - std::string friendly_name{}; - uint32_t legacy_voice_assistant_version{0}; +#endif + StringRef manufacturer_ref_{}; + void set_manufacturer(const StringRef &ref) { this->manufacturer_ref_ = ref; } + StringRef friendly_name_ref_{}; + void set_friendly_name(const StringRef &ref) { this->friendly_name_ref_ = ref; } +#ifdef USE_VOICE_ASSISTANT uint32_t voice_assistant_feature_flags{0}; - std::string suggested_area{}; - std::string bluetooth_mac_address{}; +#endif +#ifdef USE_AREAS + StringRef suggested_area_ref_{}; + void set_suggested_area(const StringRef &ref) { this->suggested_area_ref_ = ref; } +#endif +#ifdef USE_BLUETOOTH_PROXY + StringRef bluetooth_mac_address_ref_{}; + void set_bluetooth_mac_address(const StringRef &ref) { this->bluetooth_mac_address_ref_ = ref; } +#endif +#ifdef USE_API_NOISE bool api_encryption_supported{false}; - std::vector devices{}; - std::vector areas{}; +#endif +#ifdef USE_DEVICES + std::array devices{}; +#endif +#ifdef USE_AREAS + std::array areas{}; +#endif +#ifdef USE_AREAS AreaInfo area{}; +#endif void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -559,14 +602,15 @@ class SubscribeStatesRequest : public ProtoMessage { class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 12; - static constexpr uint8_t ESTIMATED_SIZE = 60; + static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_binary_sensor_response"; } #endif - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } bool is_status_binary_sensor{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -583,7 +627,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage { bool state{false}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -595,17 +639,18 @@ class BinarySensorStateResponse : public StateResponseProtoMessage { class ListEntitiesCoverResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 13; - static constexpr uint8_t ESTIMATED_SIZE = 66; + static constexpr uint8_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_cover_response"; } #endif bool assumed_state{false}; bool supports_position{false}; bool supports_tilt{false}; - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } bool supports_stop{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -615,16 +660,15 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage { class CoverStateResponse : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 22; - static constexpr uint8_t ESTIMATED_SIZE = 23; + 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{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -634,12 +678,10 @@ class CoverStateResponse : public StateResponseProtoMessage { class CoverCommandRequest : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 30; - static constexpr uint8_t ESTIMATED_SIZE = 29; + static constexpr uint8_t ESTIMATED_SIZE = 25; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "cover_command_request"; } #endif - bool has_legacy_command{false}; - enums::LegacyCoverCommand legacy_command{}; bool has_position{false}; float position{0.0f}; bool has_tilt{false}; @@ -658,7 +700,7 @@ class CoverCommandRequest : public CommandProtoMessage { class ListEntitiesFanResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 14; - static constexpr uint8_t ESTIMATED_SIZE = 77; + static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_fan_response"; } #endif @@ -666,9 +708,9 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage { bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; - std::vector supported_preset_modes{}; + const std::set *supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -678,18 +720,18 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage { class FanStateResponse : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 23; - static constexpr uint8_t ESTIMATED_SIZE = 30; + 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{}; + StringRef preset_mode_ref_{}; + void set_preset_mode(const StringRef &ref) { this->preset_mode_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -699,14 +741,12 @@ class FanStateResponse : public StateResponseProtoMessage { class FanCommandRequest : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 31; - static constexpr uint8_t ESTIMATED_SIZE = 42; + static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "fan_command_request"; } #endif 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}; @@ -729,20 +769,16 @@ class FanCommandRequest : public CommandProtoMessage { class ListEntitiesLightResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 15; - static constexpr uint8_t ESTIMATED_SIZE = 90; + static constexpr uint8_t ESTIMATED_SIZE = 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}; + const std::set *supported_color_modes{}; float min_mireds{0.0f}; float max_mireds{0.0f}; std::vector effects{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -767,9 +803,10 @@ class LightStateResponse : public StateResponseProtoMessage { float color_temperature{0.0f}; float cold_white{0.0f}; float warm_white{0.0f}; - std::string effect{}; + StringRef effect_ref_{}; + void set_effect(const StringRef &ref) { this->effect_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -823,18 +860,19 @@ class LightCommandRequest : public CommandProtoMessage { class ListEntitiesSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 16; - static constexpr uint8_t ESTIMATED_SIZE = 77; + static constexpr uint8_t ESTIMATED_SIZE = 66; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_sensor_response"; } #endif - std::string unit_of_measurement{}; + StringRef unit_of_measurement_ref_{}; + void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; } int32_t accuracy_decimals{0}; bool force_update{false}; - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } 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; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -851,7 +889,7 @@ class SensorStateResponse : public StateResponseProtoMessage { float state{0.0f}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -863,14 +901,15 @@ class SensorStateResponse : public StateResponseProtoMessage { class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 17; - static constexpr uint8_t ESTIMATED_SIZE = 60; + static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_switch_response"; } #endif bool assumed_state{false}; - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -886,7 +925,7 @@ class SwitchStateResponse : public StateResponseProtoMessage { #endif bool state{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -914,13 +953,14 @@ class SwitchCommandRequest : public CommandProtoMessage { class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 18; - static constexpr uint8_t ESTIMATED_SIZE = 58; + static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -934,10 +974,11 @@ class TextSensorStateResponse : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "text_sensor_state_response"; } #endif - std::string state{}; + StringRef state_ref_{}; + void set_state(const StringRef &ref) { this->state_ref_ = ref; } bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -945,7 +986,7 @@ class TextSensorStateResponse : public StateResponseProtoMessage { protected: }; #endif -class SubscribeLogsRequest : public ProtoMessage { +class SubscribeLogsRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 28; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -964,15 +1005,19 @@ class SubscribeLogsRequest : public ProtoMessage { class SubscribeLogsResponse : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 29; - static constexpr uint8_t ESTIMATED_SIZE = 13; + static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_logs_response"; } #endif enums::LogLevel level{}; - std::string message{}; - bool send_failed{false}; + const uint8_t *message_ptr_{nullptr}; + size_t message_len_{0}; + void set_message(const uint8_t *data, size_t len) { + this->message_ptr_ = data; + this->message_len_ = len; + } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -980,7 +1025,7 @@ class SubscribeLogsResponse : public ProtoMessage { protected: }; #ifdef USE_API_NOISE -class NoiseEncryptionSetKeyRequest : public ProtoMessage { +class NoiseEncryptionSetKeyRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 124; static constexpr uint8_t ESTIMATED_SIZE = 9; @@ -1004,7 +1049,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { #endif bool success{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1012,6 +1057,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { protected: }; #endif +#ifdef USE_API_HOMEASSISTANT_SERVICES class SubscribeHomeassistantServicesRequest : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 34; @@ -1027,16 +1073,16 @@ class SubscribeHomeassistantServicesRequest : public ProtoMessage { }; class HomeassistantServiceMap : public ProtoMessage { public: - std::string key{}; + StringRef key_ref_{}; + void set_key(const StringRef &ref) { this->key_ref_ = ref; } std::string value{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; class HomeassistantServiceResponse : public ProtoMessage { public: @@ -1045,19 +1091,22 @@ class HomeassistantServiceResponse : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "homeassistant_service_response"; } #endif - std::string service{}; + StringRef service_ref_{}; + void set_service(const StringRef &ref) { this->service_ref_ = ref; } std::vector data{}; std::vector data_template{}; std::vector variables{}; bool is_event{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; +#endif +#ifdef USE_API_HOMEASSISTANT_STATES class SubscribeHomeAssistantStatesRequest : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 38; @@ -1078,18 +1127,20 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_home_assistant_state_response"; } #endif - std::string entity_id{}; - std::string attribute{}; + StringRef entity_id_ref_{}; + void set_entity_id(const StringRef &ref) { this->entity_id_ref_ = ref; } + StringRef attribute_ref_{}; + void set_attribute(const StringRef &ref) { this->attribute_ref_ = ref; } bool once{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; -class HomeAssistantStateResponse : public ProtoMessage { +class HomeAssistantStateResponse : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 40; static constexpr uint8_t ESTIMATED_SIZE = 27; @@ -1106,6 +1157,7 @@ class HomeAssistantStateResponse : public ProtoMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; +#endif class GetTimeRequest : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 36; @@ -1119,7 +1171,7 @@ class GetTimeRequest : public ProtoMessage { protected: }; -class GetTimeResponse : public ProtoMessage { +class GetTimeResponse : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; static constexpr uint8_t ESTIMATED_SIZE = 5; @@ -1128,7 +1180,7 @@ class GetTimeResponse : public ProtoMessage { #endif uint32_t epoch_seconds{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1139,17 +1191,16 @@ class GetTimeResponse : public ProtoMessage { #ifdef USE_API_SERVICES class ListEntitiesServicesArgument : public ProtoMessage { public: - std::string name{}; + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } enums::ServiceArgType type{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class ListEntitiesServicesResponse : public ProtoMessage { public: @@ -1158,18 +1209,19 @@ class ListEntitiesServicesResponse : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_services_response"; } #endif - std::string name{}; + StringRef name_ref_{}; + void set_name(const StringRef &ref) { this->name_ref_ = ref; } uint32_t key{0}; std::vector args{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; -class ExecuteServiceArgument : public ProtoMessage { +class ExecuteServiceArgument : public ProtoDecodableMessage { public: bool bool_{false}; int32_t legacy_int{0}; @@ -1180,8 +1232,6 @@ class ExecuteServiceArgument : public ProtoMessage { std::vector int_array{}; std::vector float_array{}; std::vector string_array{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1191,7 +1241,7 @@ class ExecuteServiceArgument : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ExecuteServiceRequest : public ProtoMessage { +class ExecuteServiceRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 42; static constexpr uint8_t ESTIMATED_SIZE = 39; @@ -1213,12 +1263,12 @@ class ExecuteServiceRequest : public ProtoMessage { class ListEntitiesCameraResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 43; - static constexpr uint8_t ESTIMATED_SIZE = 49; + static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_camera_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1232,17 +1282,22 @@ class CameraImageResponse : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "camera_image_response"; } #endif - std::string data{}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } bool done{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; -class CameraImageRequest : public ProtoMessage { +class CameraImageRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 45; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1263,30 +1318,29 @@ class CameraImageRequest : public ProtoMessage { class ListEntitiesClimateResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 46; - static constexpr uint8_t ESTIMATED_SIZE = 156; + static constexpr uint8_t ESTIMATED_SIZE = 145; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_climate_response"; } #endif bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; - std::vector supported_modes{}; + const std::set *supported_modes{}; 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{}; - std::vector supported_custom_fan_modes{}; - std::vector supported_presets{}; - std::vector supported_custom_presets{}; + const std::set *supported_fan_modes{}; + const std::set *supported_swing_modes{}; + const std::set *supported_custom_fan_modes{}; + const std::set *supported_presets{}; + const std::set *supported_custom_presets{}; float visual_current_temperature_step{0.0f}; bool supports_current_humidity{false}; bool supports_target_humidity{false}; float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1296,7 +1350,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { class ClimateStateResponse : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 47; - static constexpr uint8_t ESTIMATED_SIZE = 70; + static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "climate_state_response"; } #endif @@ -1305,17 +1359,18 @@ 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{}; - std::string custom_fan_mode{}; + StringRef custom_fan_mode_ref_{}; + void set_custom_fan_mode(const StringRef &ref) { this->custom_fan_mode_ref_ = ref; } enums::ClimatePreset preset{}; - std::string custom_preset{}; + StringRef custom_preset_ref_{}; + void set_custom_preset(const StringRef &ref) { this->custom_preset_ref_ = ref; } float current_humidity{0.0f}; float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1325,7 +1380,7 @@ class ClimateStateResponse : public StateResponseProtoMessage { class ClimateCommandRequest : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 48; - static constexpr uint8_t ESTIMATED_SIZE = 88; + static constexpr uint8_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "climate_command_request"; } #endif @@ -1337,8 +1392,6 @@ class ClimateCommandRequest : public CommandProtoMessage { 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}; @@ -1365,18 +1418,20 @@ class ClimateCommandRequest : public CommandProtoMessage { class ListEntitiesNumberResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 49; - static constexpr uint8_t ESTIMATED_SIZE = 84; + static constexpr uint8_t ESTIMATED_SIZE = 75; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_number_response"; } #endif float min_value{0.0f}; float max_value{0.0f}; float step{0.0f}; - std::string unit_of_measurement{}; + StringRef unit_of_measurement_ref_{}; + void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; } enums::NumberMode mode{}; - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1393,7 +1448,7 @@ class NumberStateResponse : public StateResponseProtoMessage { float state{0.0f}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1421,13 +1476,13 @@ class NumberCommandRequest : public CommandProtoMessage { class ListEntitiesSelectResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 52; - static constexpr uint8_t ESTIMATED_SIZE = 67; + static constexpr uint8_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_select_response"; } #endif - std::vector options{}; + const std::vector *options{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1441,10 +1496,11 @@ class SelectStateResponse : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "select_state_response"; } #endif - std::string state{}; + StringRef state_ref_{}; + void set_state(const StringRef &ref) { this->state_ref_ = ref; } bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1473,7 +1529,7 @@ class SelectCommandRequest : public CommandProtoMessage { class ListEntitiesSirenResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 55; - static constexpr uint8_t ESTIMATED_SIZE = 71; + static constexpr uint8_t ESTIMATED_SIZE = 62; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_siren_response"; } #endif @@ -1481,7 +1537,7 @@ class ListEntitiesSirenResponse : public InfoResponseProtoMessage { bool supports_duration{false}; bool supports_volume{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1497,7 +1553,7 @@ class SirenStateResponse : public StateResponseProtoMessage { #endif bool state{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1533,16 +1589,17 @@ class SirenCommandRequest : public CommandProtoMessage { class ListEntitiesLockResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 58; - static constexpr uint8_t ESTIMATED_SIZE = 64; + static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_lock_response"; } #endif bool assumed_state{false}; bool supports_open{false}; bool requires_code{false}; - std::string code_format{}; + StringRef code_format_ref_{}; + void set_code_format(const StringRef &ref) { this->code_format_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1558,7 +1615,7 @@ class LockStateResponse : public StateResponseProtoMessage { #endif enums::LockState state{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1589,13 +1646,14 @@ class LockCommandRequest : public CommandProtoMessage { class ListEntitiesButtonResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 61; - static constexpr uint8_t ESTIMATED_SIZE = 58; + static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_button_response"; } #endif - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1621,32 +1679,32 @@ class ButtonCommandRequest : public CommandProtoMessage { #ifdef USE_MEDIA_PLAYER class MediaPlayerSupportedFormat : public ProtoMessage { public: - std::string format{}; + StringRef format_ref_{}; + void set_format(const StringRef &ref) { this->format_ref_ = ref; } uint32_t sample_rate{0}; uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; uint32_t sample_bytes{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 63; - static constexpr uint8_t ESTIMATED_SIZE = 85; + static constexpr uint8_t ESTIMATED_SIZE = 80; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_media_player_response"; } #endif bool supports_pause{false}; std::vector supported_formats{}; + uint32_t feature_flags{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1664,7 +1722,7 @@ class MediaPlayerStateResponse : public StateResponseProtoMessage { float volume{0.0f}; bool muted{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1697,7 +1755,7 @@ class MediaPlayerCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_BLUETOOTH_PROXY -class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { +class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 66; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1712,76 +1770,39 @@ 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 uint8_t MESSAGE_TYPE = 67; - static constexpr uint8_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: -}; 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; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothLERawAdvertisementsResponse : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 93; - static constexpr uint8_t ESTIMATED_SIZE = 34; + static constexpr uint8_t ESTIMATED_SIZE = 136; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } #endif - std::vector advertisements{}; + std::array advertisements{}; + uint16_t advertisements_len{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; -class BluetoothDeviceRequest : public ProtoMessage { +class BluetoothDeviceRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 68; static constexpr uint8_t ESTIMATED_SIZE = 12; @@ -1811,14 +1832,14 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage { uint32_t mtu{0}; int32_t error{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; -class BluetoothGATTGetServicesRequest : public ProtoMessage { +class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 70; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1835,47 +1856,45 @@ class BluetoothGATTGetServicesRequest : public ProtoMessage { }; class BluetoothGATTDescriptor : public ProtoMessage { public: - std::vector uuid{}; + std::array uuid{}; uint32_t handle{0}; + uint32_t short_uuid{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothGATTCharacteristic : public ProtoMessage { public: - std::vector uuid{}; + std::array uuid{}; uint32_t handle{0}; uint32_t properties{0}; std::vector descriptors{}; + uint32_t short_uuid{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothGATTService : public ProtoMessage { public: - std::vector uuid{}; + std::array uuid{}; uint32_t handle{0}; std::vector characteristics{}; + uint32_t short_uuid{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class BluetoothGATTGetServicesResponse : public ProtoMessage { public: @@ -1887,7 +1906,7 @@ class BluetoothGATTGetServicesResponse : public ProtoMessage { uint64_t address{0}; std::vector services{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1903,14 +1922,14 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage { #endif uint64_t address{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; -class BluetoothGATTReadRequest : public ProtoMessage { +class BluetoothGATTReadRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 73; static constexpr uint8_t ESTIMATED_SIZE = 8; @@ -1935,16 +1954,21 @@ class BluetoothGATTReadResponse : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - std::string data{}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; -class BluetoothGATTWriteRequest : public ProtoMessage { +class BluetoothGATTWriteRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 75; static constexpr uint8_t ESTIMATED_SIZE = 19; @@ -1963,7 +1987,7 @@ class BluetoothGATTWriteRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTReadDescriptorRequest : public ProtoMessage { +class BluetoothGATTReadDescriptorRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 76; static constexpr uint8_t ESTIMATED_SIZE = 8; @@ -1979,7 +2003,7 @@ class BluetoothGATTReadDescriptorRequest : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTWriteDescriptorRequest : public ProtoMessage { +class BluetoothGATTWriteDescriptorRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 77; static constexpr uint8_t ESTIMATED_SIZE = 17; @@ -1997,7 +2021,7 @@ class BluetoothGATTWriteDescriptorRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTNotifyRequest : public ProtoMessage { +class BluetoothGATTNotifyRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 78; static constexpr uint8_t ESTIMATED_SIZE = 10; @@ -2023,9 +2047,14 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - std::string data{}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2048,15 +2077,15 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { class BluetoothConnectionsFreeResponse : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 81; - static constexpr uint8_t ESTIMATED_SIZE = 16; + static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_connections_free_response"; } #endif uint32_t free{0}; uint32_t limit{0}; - std::vector allocated{}; + std::array allocated{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2074,7 +2103,7 @@ class BluetoothGATTErrorResponse : public ProtoMessage { uint32_t handle{0}; int32_t error{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2091,7 +2120,7 @@ class BluetoothGATTWriteResponse : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2108,7 +2137,7 @@ class BluetoothGATTNotifyResponse : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2126,7 +2155,7 @@ class BluetoothDevicePairingResponse : public ProtoMessage { bool paired{false}; int32_t error{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2144,7 +2173,7 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage { bool success{false}; int32_t error{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2175,7 +2204,7 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage { bool success{false}; int32_t error{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2192,14 +2221,14 @@ class BluetoothScannerStateResponse : public ProtoMessage { enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; -class BluetoothScannerSetModeRequest : public ProtoMessage { +class BluetoothScannerSetModeRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 127; static constexpr uint8_t ESTIMATED_SIZE = 2; @@ -2216,7 +2245,7 @@ class BluetoothScannerSetModeRequest : public ProtoMessage { }; #endif #ifdef USE_VOICE_ASSISTANT -class SubscribeVoiceAssistantRequest : public ProtoMessage { +class SubscribeVoiceAssistantRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 89; static constexpr uint8_t ESTIMATED_SIZE = 6; @@ -2238,14 +2267,12 @@ class VoiceAssistantAudioSettings : public ProtoMessage { uint32_t auto_gain{0}; float volume_multiplier{0.0f}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: - bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class VoiceAssistantRequest : public ProtoMessage { public: @@ -2255,19 +2282,21 @@ class VoiceAssistantRequest : public ProtoMessage { const char *message_name() const override { return "voice_assistant_request"; } #endif bool start{false}; - std::string conversation_id{}; + StringRef conversation_id_ref_{}; + void set_conversation_id(const StringRef &ref) { this->conversation_id_ref_ = ref; } uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; - std::string wake_word_phrase{}; + StringRef wake_word_phrase_ref_{}; + void set_wake_word_phrase(const StringRef &ref) { this->wake_word_phrase_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; -class VoiceAssistantResponse : public ProtoMessage { +class VoiceAssistantResponse : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 91; static constexpr uint8_t ESTIMATED_SIZE = 6; @@ -2283,12 +2312,10 @@ class VoiceAssistantResponse : public ProtoMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantEventData : public ProtoMessage { +class VoiceAssistantEventData : public ProtoDecodableMessage { public: std::string name{}; std::string value{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2296,7 +2323,7 @@ class VoiceAssistantEventData : public ProtoMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class VoiceAssistantEventResponse : public ProtoMessage { +class VoiceAssistantEventResponse : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 92; static constexpr uint8_t ESTIMATED_SIZE = 36; @@ -2313,7 +2340,7 @@ class VoiceAssistantEventResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantAudio : public ProtoMessage { +class VoiceAssistantAudio : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 106; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -2321,9 +2348,15 @@ class VoiceAssistantAudio : public ProtoMessage { const char *message_name() const override { return "voice_assistant_audio"; } #endif std::string data{}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } bool end{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2332,7 +2365,7 @@ class VoiceAssistantAudio : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantTimerEventResponse : public ProtoMessage { +class VoiceAssistantTimerEventResponse : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 115; static constexpr uint8_t ESTIMATED_SIZE = 30; @@ -2353,7 +2386,7 @@ class VoiceAssistantTimerEventResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantAnnounceRequest : public ProtoMessage { +class VoiceAssistantAnnounceRequest : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 119; static constexpr uint8_t ESTIMATED_SIZE = 29; @@ -2381,7 +2414,7 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage { #endif bool success{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2390,17 +2423,18 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage { }; class VoiceAssistantWakeWord : public ProtoMessage { public: - std::string id{}; - std::string wake_word{}; + StringRef id_ref_{}; + void set_id(const StringRef &ref) { this->id_ref_ = ref; } + StringRef wake_word_ref_{}; + void set_wake_word(const StringRef &ref) { this->wake_word_ref_ = ref; } std::vector trained_languages{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; class VoiceAssistantConfigurationRequest : public ProtoMessage { public: @@ -2423,17 +2457,17 @@ class VoiceAssistantConfigurationResponse : public ProtoMessage { const char *message_name() const override { return "voice_assistant_configuration_response"; } #endif std::vector available_wake_words{}; - std::vector active_wake_words{}; + const std::vector *active_wake_words{}; uint32_t max_active_wake_words{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif protected: }; -class VoiceAssistantSetConfiguration : public ProtoMessage { +class VoiceAssistantSetConfiguration : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 123; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -2453,7 +2487,7 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 94; - static constexpr uint8_t ESTIMATED_SIZE = 57; + static constexpr uint8_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_alarm_control_panel_response"; } #endif @@ -2461,7 +2495,7 @@ class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { bool requires_code{false}; bool requires_code_to_arm{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2477,7 +2511,7 @@ class AlarmControlPanelStateResponse : public StateResponseProtoMessage { #endif enums::AlarmControlPanelState state{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2507,16 +2541,17 @@ class AlarmControlPanelCommandRequest : public CommandProtoMessage { class ListEntitiesTextResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 97; - static constexpr uint8_t ESTIMATED_SIZE = 68; + static constexpr uint8_t ESTIMATED_SIZE = 59; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_text_response"; } #endif uint32_t min_length{0}; uint32_t max_length{0}; - std::string pattern{}; + StringRef pattern_ref_{}; + void set_pattern(const StringRef &ref) { this->pattern_ref_ = ref; } enums::TextMode mode{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2530,10 +2565,11 @@ class TextStateResponse : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "text_state_response"; } #endif - std::string state{}; + StringRef state_ref_{}; + void set_state(const StringRef &ref) { this->state_ref_ = ref; } bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2562,12 +2598,12 @@ class TextCommandRequest : public CommandProtoMessage { class ListEntitiesDateResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 100; - static constexpr uint8_t ESTIMATED_SIZE = 49; + static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2586,7 +2622,7 @@ class DateStateResponse : public StateResponseProtoMessage { uint32_t month{0}; uint32_t day{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2616,12 +2652,12 @@ class DateCommandRequest : public CommandProtoMessage { class ListEntitiesTimeResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 103; - static constexpr uint8_t ESTIMATED_SIZE = 49; + static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_time_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2640,7 +2676,7 @@ class TimeStateResponse : public StateResponseProtoMessage { uint32_t minute{0}; uint32_t second{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2670,14 +2706,15 @@ class TimeCommandRequest : public CommandProtoMessage { class ListEntitiesEventResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 107; - static constexpr uint8_t ESTIMATED_SIZE = 76; + static constexpr uint8_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_event_response"; } #endif - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } std::vector event_types{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2691,9 +2728,10 @@ class EventResponse : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "event_response"; } #endif - std::string event_type{}; + StringRef event_type_ref_{}; + void set_event_type(const StringRef &ref) { this->event_type_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2705,16 +2743,17 @@ class EventResponse : public StateResponseProtoMessage { class ListEntitiesValveResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 109; - static constexpr uint8_t ESTIMATED_SIZE = 64; + static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_valve_response"; } #endif - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2731,7 +2770,7 @@ class ValveStateResponse : public StateResponseProtoMessage { float position{0.0f}; enums::ValveOperation current_operation{}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2761,12 +2800,12 @@ class ValveCommandRequest : public CommandProtoMessage { class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 112; - static constexpr uint8_t ESTIMATED_SIZE = 49; + static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_date_time_response"; } #endif void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2783,7 +2822,7 @@ class DateTimeStateResponse : public StateResponseProtoMessage { bool missing_state{false}; uint32_t epoch_seconds{0}; void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2811,13 +2850,14 @@ class DateTimeCommandRequest : public CommandProtoMessage { class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 116; - static constexpr uint8_t ESTIMATED_SIZE = 58; + static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_update_response"; } #endif - std::string device_class{}; + StringRef device_class_ref_{}; + void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2835,13 +2875,18 @@ class UpdateStateResponse : public StateResponseProtoMessage { bool in_progress{false}; bool has_progress{false}; float progress{0.0f}; - std::string current_version{}; - std::string latest_version{}; - std::string title{}; - std::string release_summary{}; - std::string release_url{}; + StringRef current_version_ref_{}; + void set_current_version(const StringRef &ref) { this->current_version_ref_ = ref; } + StringRef latest_version_ref_{}; + void set_latest_version(const StringRef &ref) { this->latest_version_ref_ = ref; } + StringRef title_ref_{}; + void set_title(const StringRef &ref) { this->title_ref_ = ref; } + StringRef release_summary_ref_{}; + void set_release_summary(const StringRef &ref) { this->release_summary_ref_ = ref; } + StringRef release_url_ref_{}; + void set_release_url(const StringRef &ref) { this->release_url_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(uint32_t &total_size) const override; + void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2866,5 +2911,4 @@ class UpdateCommandRequest : public CommandProtoMessage { }; #endif -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index f6509f47cc..7af322f96d 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -7,8 +7,92 @@ #ifdef HAS_PROTO_MESSAGE_DUMP -namespace esphome { -namespace api { +namespace esphome::api { + +// Helper function to append a quoted string, handling empty StringRef +static inline void append_quoted_string(std::string &out, const StringRef &ref) { + out.append("'"); + if (!ref.empty()) { + out.append(ref.c_str()); + } + out.append("'"); +} + +// Common helpers for dump_field functions +static inline void append_field_prefix(std::string &out, const char *field_name, int indent) { + out.append(indent, ' ').append(field_name).append(": "); +} + +static inline void append_with_newline(std::string &out, const char *str) { + out.append(str); + out.append("\n"); +} + +// RAII helper for message dump formatting +class MessageDumpHelper { + public: + MessageDumpHelper(std::string &out, const char *message_name) : out_(out) { + out_.append(message_name); + out_.append(" {\n"); + } + ~MessageDumpHelper() { out_.append(" }"); } + + private: + std::string &out_; +}; + +// Helper functions to reduce code duplication in dump methods +static void dump_field(std::string &out, const char *field_name, int32_t value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%" PRId32, value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, uint32_t value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%" PRIu32, value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, float value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%g", value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%llu", value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, bool value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append(YESNO(value)); + out.append("\n"); +} + +static void dump_field(std::string &out, const char *field_name, const std::string &value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append("'").append(value).append("'"); + out.append("\n"); +} + +static void dump_field(std::string &out, const char *field_name, StringRef value, int indent = 2) { + append_field_prefix(out, field_name, indent); + append_quoted_string(out, value); + out.append("\n"); +} + +template static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append(proto_enum_to_string(value)); + out.append("\n"); +} template<> const char *proto_enum_to_string(enums::EntityCategory value) { switch (value) { @@ -23,16 +107,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 +119,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 +177,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) { @@ -345,6 +383,12 @@ template<> const char *proto_enum_to_string(enums::Medi return "MEDIA_PLAYER_STATE_PLAYING"; case enums::MEDIA_PLAYER_STATE_PAUSED: return "MEDIA_PLAYER_STATE_PAUSED"; + case enums::MEDIA_PLAYER_STATE_ANNOUNCING: + return "MEDIA_PLAYER_STATE_ANNOUNCING"; + case enums::MEDIA_PLAYER_STATE_OFF: + return "MEDIA_PLAYER_STATE_OFF"; + case enums::MEDIA_PLAYER_STATE_ON: + return "MEDIA_PLAYER_STATE_ON"; default: return "UNKNOWN"; } @@ -361,6 +405,24 @@ template<> const char *proto_enum_to_string(enums::Me return "MEDIA_PLAYER_COMMAND_MUTE"; case enums::MEDIA_PLAYER_COMMAND_UNMUTE: return "MEDIA_PLAYER_COMMAND_UNMUTE"; + case enums::MEDIA_PLAYER_COMMAND_TOGGLE: + return "MEDIA_PLAYER_COMMAND_TOGGLE"; + case enums::MEDIA_PLAYER_COMMAND_VOLUME_UP: + return "MEDIA_PLAYER_COMMAND_VOLUME_UP"; + case enums::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: + return "MEDIA_PLAYER_COMMAND_VOLUME_DOWN"; + case enums::MEDIA_PLAYER_COMMAND_ENQUEUE: + return "MEDIA_PLAYER_COMMAND_ENQUEUE"; + case enums::MEDIA_PLAYER_COMMAND_REPEAT_ONE: + return "MEDIA_PLAYER_COMMAND_REPEAT_ONE"; + case enums::MEDIA_PLAYER_COMMAND_REPEAT_OFF: + return "MEDIA_PLAYER_COMMAND_REPEAT_OFF"; + case enums::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST: + return "MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST"; + case enums::MEDIA_PLAYER_COMMAND_TURN_ON: + return "MEDIA_PLAYER_COMMAND_TURN_ON"; + case enums::MEDIA_PLAYER_COMMAND_TURN_OFF: + return "MEDIA_PLAYER_COMMAND_TURN_OFF"; default: return "UNKNOWN"; } @@ -595,3838 +657,1447 @@ template<> const char *proto_enum_to_string(enums::UpdateC #endif void HelloRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HelloRequest {\n"); - out.append(" client_info: "); - out.append("'").append(this->client_info).append("'"); - out.append("\n"); - - out.append(" api_version_major: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_major); - out.append(buffer); - out.append("\n"); - - out.append(" api_version_minor: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_minor); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "HelloRequest"); + dump_field(out, "client_info", this->client_info); + dump_field(out, "api_version_major", this->api_version_major); + dump_field(out, "api_version_minor", this->api_version_minor); } void HelloResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HelloResponse {\n"); - out.append(" api_version_major: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_major); - out.append(buffer); - out.append("\n"); - - out.append(" api_version_minor: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->api_version_minor); - out.append(buffer); - out.append("\n"); - - out.append(" server_info: "); - out.append("'").append(this->server_info).append("'"); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - out.append("}"); -} -void ConnectRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ConnectRequest {\n"); - out.append(" password: "); - out.append("'").append(this->password).append("'"); - out.append("\n"); - out.append("}"); -} -void ConnectResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ConnectResponse {\n"); - out.append(" invalid_password: "); - out.append(YESNO(this->invalid_password)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "HelloResponse"); + dump_field(out, "api_version_major", this->api_version_major); + dump_field(out, "api_version_minor", this->api_version_minor); + dump_field(out, "server_info", this->server_info_ref_); + dump_field(out, "name", this->name_ref_); } +void ConnectRequest::dump_to(std::string &out) const { dump_field(out, "password", this->password); } +void ConnectResponse::dump_to(std::string &out) const { dump_field(out, "invalid_password", this->invalid_password); } void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } 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"); - out.append(" area_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->area_id); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "AreaInfo"); + dump_field(out, "area_id", this->area_id); + dump_field(out, "name", this->name_ref_); } +#endif +#ifdef USE_DEVICES void DeviceInfo::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DeviceInfo {\n"); - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" area_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->area_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "DeviceInfo"); + dump_field(out, "device_id", this->device_id); + dump_field(out, "name", this->name_ref_); + dump_field(out, "area_id", this->area_id); } +#endif void DeviceInfoResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DeviceInfoResponse {\n"); - out.append(" uses_password: "); - out.append(YESNO(this->uses_password)); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" mac_address: "); - out.append("'").append(this->mac_address).append("'"); - out.append("\n"); - - out.append(" esphome_version: "); - out.append("'").append(this->esphome_version).append("'"); - out.append("\n"); - - out.append(" compilation_time: "); - out.append("'").append(this->compilation_time).append("'"); - out.append("\n"); - - out.append(" model: "); - out.append("'").append(this->model).append("'"); - out.append("\n"); - - out.append(" has_deep_sleep: "); - out.append(YESNO(this->has_deep_sleep)); - out.append("\n"); - - out.append(" project_name: "); - out.append("'").append(this->project_name).append("'"); - out.append("\n"); - - out.append(" project_version: "); - out.append("'").append(this->project_version).append("'"); - out.append("\n"); - - 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"); - - out.append(" bluetooth_proxy_feature_flags: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->bluetooth_proxy_feature_flags); - out.append(buffer); - out.append("\n"); - - out.append(" manufacturer: "); - out.append("'").append(this->manufacturer).append("'"); - out.append("\n"); - - out.append(" friendly_name: "); - 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"); - - out.append(" voice_assistant_feature_flags: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->voice_assistant_feature_flags); - out.append(buffer); - out.append("\n"); - - out.append(" suggested_area: "); - out.append("'").append(this->suggested_area).append("'"); - out.append("\n"); - - out.append(" bluetooth_mac_address: "); - out.append("'").append(this->bluetooth_mac_address).append("'"); - out.append("\n"); - - out.append(" api_encryption_supported: "); - out.append(YESNO(this->api_encryption_supported)); - out.append("\n"); - + MessageDumpHelper helper(out, "DeviceInfoResponse"); +#ifdef USE_API_PASSWORD + dump_field(out, "uses_password", this->uses_password); +#endif + dump_field(out, "name", this->name_ref_); + dump_field(out, "mac_address", this->mac_address_ref_); + dump_field(out, "esphome_version", this->esphome_version_ref_); + dump_field(out, "compilation_time", this->compilation_time_ref_); + dump_field(out, "model", this->model_ref_); +#ifdef USE_DEEP_SLEEP + dump_field(out, "has_deep_sleep", this->has_deep_sleep); +#endif +#ifdef ESPHOME_PROJECT_NAME + dump_field(out, "project_name", this->project_name_ref_); +#endif +#ifdef ESPHOME_PROJECT_NAME + dump_field(out, "project_version", this->project_version_ref_); +#endif +#ifdef USE_WEBSERVER + dump_field(out, "webserver_port", this->webserver_port); +#endif +#ifdef USE_BLUETOOTH_PROXY + dump_field(out, "bluetooth_proxy_feature_flags", this->bluetooth_proxy_feature_flags); +#endif + dump_field(out, "manufacturer", this->manufacturer_ref_); + dump_field(out, "friendly_name", this->friendly_name_ref_); +#ifdef USE_VOICE_ASSISTANT + dump_field(out, "voice_assistant_feature_flags", this->voice_assistant_feature_flags); +#endif +#ifdef USE_AREAS + dump_field(out, "suggested_area", this->suggested_area_ref_); +#endif +#ifdef USE_BLUETOOTH_PROXY + dump_field(out, "bluetooth_mac_address", this->bluetooth_mac_address_ref_); +#endif +#ifdef USE_API_NOISE + dump_field(out, "api_encryption_supported", this->api_encryption_supported); +#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"); - out.append("}"); +#endif } void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); } void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesBinarySensorResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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"); - - out.append(" is_status_binary_sensor: "); - out.append(YESNO(this->is_status_binary_sensor)); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); + dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "is_status_binary_sensor", this->is_status_binary_sensor); + dump_field(out, "disabled_by_default", this->disabled_by_default); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "entity_category", static_cast(this->entity_category)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void BinarySensorStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BinarySensorStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BinarySensorStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); + dump_field(out, "missing_state", this->missing_state); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_COVER void ListEntitiesCoverResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesCoverResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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"); - - out.append(" supports_position: "); - out.append(YESNO(this->supports_position)); - out.append("\n"); - - out.append(" supports_tilt: "); - out.append(YESNO(this->supports_tilt)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" supports_stop: "); - out.append(YESNO(this->supports_stop)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesCoverResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); + dump_field(out, "assumed_state", this->assumed_state); + dump_field(out, "supports_position", this->supports_position); + dump_field(out, "supports_tilt", this->supports_tilt); + dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "disabled_by_default", this->disabled_by_default); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "supports_stop", this->supports_stop); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void CoverStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("CoverStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - 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); - out.append("\n"); - - out.append(" tilt: "); - snprintf(buffer, sizeof(buffer), "%g", this->tilt); - out.append(buffer); - out.append("\n"); - - out.append(" current_operation: "); - out.append(proto_enum_to_string(this->current_operation)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "CoverStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "position", this->position); + dump_field(out, "tilt", this->tilt); + dump_field(out, "current_operation", static_cast(this->current_operation)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void CoverCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("CoverCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - 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"); - - out.append(" position: "); - snprintf(buffer, sizeof(buffer), "%g", this->position); - out.append(buffer); - out.append("\n"); - - out.append(" has_tilt: "); - out.append(YESNO(this->has_tilt)); - out.append("\n"); - - out.append(" tilt: "); - snprintf(buffer, sizeof(buffer), "%g", this->tilt); - out.append(buffer); - out.append("\n"); - - out.append(" stop: "); - out.append(YESNO(this->stop)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "CoverCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_position", this->has_position); + dump_field(out, "position", this->position); + dump_field(out, "has_tilt", this->has_tilt); + dump_field(out, "tilt", this->tilt); + dump_field(out, "stop", this->stop); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_FAN void ListEntitiesFanResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesFanResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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"); - - out.append(" supports_speed: "); - out.append(YESNO(this->supports_speed)); - out.append("\n"); - - out.append(" supports_direction: "); - out.append(YESNO(this->supports_direction)); - out.append("\n"); - - out.append(" supported_speed_count: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->supported_speed_count); - out.append(buffer); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - for (const auto &it : this->supported_preset_modes) { - out.append(" supported_preset_modes: "); - out.append("'").append(it).append("'"); - out.append("\n"); + MessageDumpHelper helper(out, "ListEntitiesFanResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); + dump_field(out, "supports_oscillation", this->supports_oscillation); + dump_field(out, "supports_speed", this->supports_speed); + dump_field(out, "supports_direction", this->supports_direction); + dump_field(out, "supported_speed_count", this->supported_speed_count); + dump_field(out, "disabled_by_default", this->disabled_by_default); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "entity_category", static_cast(this->entity_category)); + for (const auto &it : *this->supported_preset_modes) { + dump_field(out, "supported_preset_modes", it, 4); } - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void FanStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("FanStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" oscillating: "); - 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"); - - out.append(" speed_level: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->speed_level); - out.append(buffer); - out.append("\n"); - - out.append(" preset_mode: "); - out.append("'").append(this->preset_mode).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "FanStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); + dump_field(out, "oscillating", this->oscillating); + dump_field(out, "direction", static_cast(this->direction)); + dump_field(out, "speed_level", this->speed_level); + dump_field(out, "preset_mode", this->preset_mode_ref_); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void FanCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("FanCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_state: "); - out.append(YESNO(this->has_state)); - out.append("\n"); - - out.append(" state: "); - 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"); - - out.append(" oscillating: "); - out.append(YESNO(this->oscillating)); - out.append("\n"); - - out.append(" has_direction: "); - out.append(YESNO(this->has_direction)); - out.append("\n"); - - out.append(" direction: "); - out.append(proto_enum_to_string(this->direction)); - out.append("\n"); - - out.append(" has_speed_level: "); - out.append(YESNO(this->has_speed_level)); - out.append("\n"); - - out.append(" speed_level: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->speed_level); - out.append(buffer); - out.append("\n"); - - out.append(" has_preset_mode: "); - out.append(YESNO(this->has_preset_mode)); - out.append("\n"); - - out.append(" preset_mode: "); - out.append("'").append(this->preset_mode).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "FanCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_state", this->has_state); + dump_field(out, "state", this->state); + dump_field(out, "has_oscillating", this->has_oscillating); + dump_field(out, "oscillating", this->oscillating); + dump_field(out, "has_direction", this->has_direction); + dump_field(out, "direction", static_cast(this->direction)); + dump_field(out, "has_speed_level", this->has_speed_level); + dump_field(out, "speed_level", this->speed_level); + dump_field(out, "has_preset_mode", this->has_preset_mode); + dump_field(out, "preset_mode", this->preset_mode); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_LIGHT void ListEntitiesLightResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesLightResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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"); + MessageDumpHelper helper(out, "ListEntitiesLightResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); + for (const auto &it : *this->supported_color_modes) { + dump_field(out, "supported_color_modes", static_cast(it), 4); } - - 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); - out.append("\n"); - - out.append(" max_mireds: "); - snprintf(buffer, sizeof(buffer), "%g", this->max_mireds); - out.append(buffer); - out.append("\n"); - + dump_field(out, "min_mireds", this->min_mireds); + dump_field(out, "max_mireds", this->max_mireds); for (const auto &it : this->effects) { - out.append(" effects: "); - out.append("'").append(it).append("'"); - out.append("\n"); + dump_field(out, "effects", it, 4); } - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + dump_field(out, "disabled_by_default", this->disabled_by_default); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "entity_category", static_cast(this->entity_category)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void LightStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("LightStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" brightness: "); - snprintf(buffer, sizeof(buffer), "%g", this->brightness); - out.append(buffer); - out.append("\n"); - - out.append(" color_mode: "); - out.append(proto_enum_to_string(this->color_mode)); - out.append("\n"); - - out.append(" color_brightness: "); - snprintf(buffer, sizeof(buffer), "%g", this->color_brightness); - out.append(buffer); - out.append("\n"); - - out.append(" red: "); - snprintf(buffer, sizeof(buffer), "%g", this->red); - out.append(buffer); - out.append("\n"); - - out.append(" green: "); - snprintf(buffer, sizeof(buffer), "%g", this->green); - out.append(buffer); - out.append("\n"); - - out.append(" blue: "); - snprintf(buffer, sizeof(buffer), "%g", this->blue); - out.append(buffer); - out.append("\n"); - - out.append(" white: "); - snprintf(buffer, sizeof(buffer), "%g", this->white); - out.append(buffer); - out.append("\n"); - - out.append(" color_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->color_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" cold_white: "); - snprintf(buffer, sizeof(buffer), "%g", this->cold_white); - out.append(buffer); - out.append("\n"); - - out.append(" warm_white: "); - snprintf(buffer, sizeof(buffer), "%g", this->warm_white); - out.append(buffer); - out.append("\n"); - - out.append(" effect: "); - out.append("'").append(this->effect).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "LightStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); + dump_field(out, "brightness", this->brightness); + dump_field(out, "color_mode", static_cast(this->color_mode)); + dump_field(out, "color_brightness", this->color_brightness); + dump_field(out, "red", this->red); + dump_field(out, "green", this->green); + dump_field(out, "blue", this->blue); + dump_field(out, "white", this->white); + dump_field(out, "color_temperature", this->color_temperature); + dump_field(out, "cold_white", this->cold_white); + dump_field(out, "warm_white", this->warm_white); + dump_field(out, "effect", this->effect_ref_); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void LightCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("LightCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_state: "); - out.append(YESNO(this->has_state)); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" has_brightness: "); - out.append(YESNO(this->has_brightness)); - out.append("\n"); - - out.append(" brightness: "); - snprintf(buffer, sizeof(buffer), "%g", this->brightness); - out.append(buffer); - out.append("\n"); - - out.append(" has_color_mode: "); - out.append(YESNO(this->has_color_mode)); - out.append("\n"); - - out.append(" color_mode: "); - out.append(proto_enum_to_string(this->color_mode)); - out.append("\n"); - - out.append(" has_color_brightness: "); - out.append(YESNO(this->has_color_brightness)); - out.append("\n"); - - out.append(" color_brightness: "); - snprintf(buffer, sizeof(buffer), "%g", this->color_brightness); - out.append(buffer); - out.append("\n"); - - out.append(" has_rgb: "); - out.append(YESNO(this->has_rgb)); - out.append("\n"); - - out.append(" red: "); - snprintf(buffer, sizeof(buffer), "%g", this->red); - out.append(buffer); - out.append("\n"); - - out.append(" green: "); - snprintf(buffer, sizeof(buffer), "%g", this->green); - out.append(buffer); - out.append("\n"); - - out.append(" blue: "); - snprintf(buffer, sizeof(buffer), "%g", this->blue); - out.append(buffer); - out.append("\n"); - - out.append(" has_white: "); - out.append(YESNO(this->has_white)); - out.append("\n"); - - out.append(" white: "); - snprintf(buffer, sizeof(buffer), "%g", this->white); - out.append(buffer); - out.append("\n"); - - out.append(" has_color_temperature: "); - out.append(YESNO(this->has_color_temperature)); - out.append("\n"); - - out.append(" color_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->color_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" has_cold_white: "); - out.append(YESNO(this->has_cold_white)); - out.append("\n"); - - out.append(" cold_white: "); - snprintf(buffer, sizeof(buffer), "%g", this->cold_white); - out.append(buffer); - out.append("\n"); - - out.append(" has_warm_white: "); - out.append(YESNO(this->has_warm_white)); - out.append("\n"); - - out.append(" warm_white: "); - snprintf(buffer, sizeof(buffer), "%g", this->warm_white); - out.append(buffer); - out.append("\n"); - - out.append(" has_transition_length: "); - out.append(YESNO(this->has_transition_length)); - out.append("\n"); - - out.append(" transition_length: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->transition_length); - out.append(buffer); - out.append("\n"); - - out.append(" has_flash_length: "); - out.append(YESNO(this->has_flash_length)); - out.append("\n"); - - out.append(" flash_length: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flash_length); - out.append(buffer); - out.append("\n"); - - out.append(" has_effect: "); - out.append(YESNO(this->has_effect)); - out.append("\n"); - - out.append(" effect: "); - out.append("'").append(this->effect).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "LightCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_state", this->has_state); + dump_field(out, "state", this->state); + dump_field(out, "has_brightness", this->has_brightness); + dump_field(out, "brightness", this->brightness); + dump_field(out, "has_color_mode", this->has_color_mode); + dump_field(out, "color_mode", static_cast(this->color_mode)); + dump_field(out, "has_color_brightness", this->has_color_brightness); + dump_field(out, "color_brightness", this->color_brightness); + dump_field(out, "has_rgb", this->has_rgb); + dump_field(out, "red", this->red); + dump_field(out, "green", this->green); + dump_field(out, "blue", this->blue); + dump_field(out, "has_white", this->has_white); + dump_field(out, "white", this->white); + dump_field(out, "has_color_temperature", this->has_color_temperature); + dump_field(out, "color_temperature", this->color_temperature); + dump_field(out, "has_cold_white", this->has_cold_white); + dump_field(out, "cold_white", this->cold_white); + dump_field(out, "has_warm_white", this->has_warm_white); + dump_field(out, "warm_white", this->warm_white); + dump_field(out, "has_transition_length", this->has_transition_length); + dump_field(out, "transition_length", this->transition_length); + dump_field(out, "has_flash_length", this->has_flash_length); + dump_field(out, "flash_length", this->flash_length); + dump_field(out, "has_effect", this->has_effect); + dump_field(out, "effect", this->effect); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_SENSOR void ListEntitiesSensorResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesSensorResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" unit_of_measurement: "); - out.append("'").append(this->unit_of_measurement).append("'"); - out.append("\n"); - - out.append(" accuracy_decimals: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->accuracy_decimals); - out.append(buffer); - out.append("\n"); - - out.append(" force_update: "); - out.append(YESNO(this->force_update)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - out.append(" state_class: "); - 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"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesSensorResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "unit_of_measurement", this->unit_of_measurement_ref_); + dump_field(out, "accuracy_decimals", this->accuracy_decimals); + dump_field(out, "force_update", this->force_update); + dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "state_class", static_cast(this->state_class)); + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void SensorStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SensorStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - snprintf(buffer, sizeof(buffer), "%g", this->state); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SensorStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); + dump_field(out, "missing_state", this->missing_state); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_SWITCH void ListEntitiesSwitchResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesSwitchResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" assumed_state: "); - out.append(YESNO(this->assumed_state)); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesSwitchResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "assumed_state", this->assumed_state); + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "device_class", this->device_class_ref_); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void SwitchStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SwitchStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SwitchStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void SwitchCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SwitchCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SwitchCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_TEXT_SENSOR void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesTextSensorResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesTextSensorResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "device_class", this->device_class_ref_); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void TextSensorStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TextSensorStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "TextSensorStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state_ref_); + dump_field(out, "missing_state", this->missing_state); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif void SubscribeLogsRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeLogsRequest {\n"); - out.append(" level: "); - out.append(proto_enum_to_string(this->level)); - out.append("\n"); - - out.append(" dump_config: "); - out.append(YESNO(this->dump_config)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SubscribeLogsRequest"); + dump_field(out, "level", static_cast(this->level)); + dump_field(out, "dump_config", this->dump_config); } void SubscribeLogsResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeLogsResponse {\n"); - out.append(" level: "); - out.append(proto_enum_to_string(this->level)); - out.append("\n"); - + MessageDumpHelper helper(out, "SubscribeLogsResponse"); + dump_field(out, "level", static_cast(this->level)); out.append(" message: "); - out.append(format_hex_pretty(this->message)); + out.append(format_hex_pretty(this->message_ptr_, this->message_len_)); out.append("\n"); - - out.append(" send_failed: "); - out.append(YESNO(this->send_failed)); - out.append("\n"); - out.append("}"); } #ifdef USE_API_NOISE void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("NoiseEncryptionSetKeyRequest {\n"); + MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest"); out.append(" key: "); - out.append(format_hex_pretty(this->key)); + out.append(format_hex_pretty(reinterpret_cast(this->key.data()), this->key.size())); out.append("\n"); - out.append("}"); -} -void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("NoiseEncryptionSetKeyResponse {\n"); - out.append(" success: "); - out.append(YESNO(this->success)); - out.append("\n"); - out.append("}"); } +void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); } #endif +#ifdef USE_API_HOMEASSISTANT_SERVICES void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeassistantServicesRequest {}"); } void HomeassistantServiceMap::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HomeassistantServiceMap {\n"); - out.append(" key: "); - out.append("'").append(this->key).append("'"); - out.append("\n"); - - out.append(" value: "); - out.append("'").append(this->value).append("'"); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "HomeassistantServiceMap"); + dump_field(out, "key", this->key_ref_); + dump_field(out, "value", this->value); } void HomeassistantServiceResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HomeassistantServiceResponse {\n"); - out.append(" service: "); - out.append("'").append(this->service).append("'"); - out.append("\n"); - + MessageDumpHelper helper(out, "HomeassistantServiceResponse"); + dump_field(out, "service", this->service_ref_); for (const auto &it : this->data) { out.append(" data: "); it.dump_to(out); out.append("\n"); } - for (const auto &it : this->data_template) { out.append(" data_template: "); it.dump_to(out); out.append("\n"); } - for (const auto &it : this->variables) { out.append(" variables: "); it.dump_to(out); out.append("\n"); } - - out.append(" is_event: "); - out.append(YESNO(this->is_event)); - out.append("\n"); - out.append("}"); + dump_field(out, "is_event", this->is_event); } +#endif +#ifdef USE_API_HOMEASSISTANT_STATES void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeAssistantStatesRequest {}"); } void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeHomeAssistantStateResponse {\n"); - out.append(" entity_id: "); - out.append("'").append(this->entity_id).append("'"); - out.append("\n"); - - out.append(" attribute: "); - out.append("'").append(this->attribute).append("'"); - out.append("\n"); - - out.append(" once: "); - out.append(YESNO(this->once)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse"); + dump_field(out, "entity_id", this->entity_id_ref_); + dump_field(out, "attribute", this->attribute_ref_); + dump_field(out, "once", this->once); } void HomeAssistantStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("HomeAssistantStateResponse {\n"); - out.append(" entity_id: "); - out.append("'").append(this->entity_id).append("'"); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - - out.append(" attribute: "); - out.append("'").append(this->attribute).append("'"); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "HomeAssistantStateResponse"); + dump_field(out, "entity_id", this->entity_id); + dump_field(out, "state", this->state); + dump_field(out, "attribute", this->attribute); } +#endif void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } -void GetTimeResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("GetTimeResponse {\n"); - out.append(" epoch_seconds: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); - out.append(buffer); - out.append("\n"); - out.append("}"); -} +void GetTimeResponse::dump_to(std::string &out) const { dump_field(out, "epoch_seconds", this->epoch_seconds); } #ifdef USE_API_SERVICES void ListEntitiesServicesArgument::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesServicesArgument {\n"); - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" type: "); - out.append(proto_enum_to_string(this->type)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); + dump_field(out, "name", this->name_ref_); + dump_field(out, "type", static_cast(this->type)); } void ListEntitiesServicesResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesServicesResponse {\n"); - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesServicesResponse"); + dump_field(out, "name", this->name_ref_); + dump_field(out, "key", this->key); for (const auto &it : this->args) { out.append(" args: "); it.dump_to(out); out.append("\n"); } - out.append("}"); } void ExecuteServiceArgument::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ExecuteServiceArgument {\n"); - out.append(" bool_: "); - out.append(YESNO(this->bool_)); - out.append("\n"); - - out.append(" legacy_int: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->legacy_int); - out.append(buffer); - out.append("\n"); - - out.append(" float_: "); - snprintf(buffer, sizeof(buffer), "%g", this->float_); - out.append(buffer); - out.append("\n"); - - out.append(" string_: "); - out.append("'").append(this->string_).append("'"); - out.append("\n"); - - out.append(" int_: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->int_); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "ExecuteServiceArgument"); + dump_field(out, "bool_", this->bool_); + dump_field(out, "legacy_int", this->legacy_int); + dump_field(out, "float_", this->float_); + dump_field(out, "string_", this->string_); + dump_field(out, "int_", this->int_); for (const auto it : this->bool_array) { - out.append(" bool_array: "); - out.append(YESNO(it)); - out.append("\n"); + dump_field(out, "bool_array", it, 4); } - for (const auto &it : this->int_array) { - out.append(" int_array: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, it); - out.append(buffer); - out.append("\n"); + dump_field(out, "int_array", it, 4); } - for (const auto &it : this->float_array) { - out.append(" float_array: "); - snprintf(buffer, sizeof(buffer), "%g", it); - out.append(buffer); - out.append("\n"); + dump_field(out, "float_array", it, 4); } - for (const auto &it : this->string_array) { - out.append(" string_array: "); - out.append("'").append(it).append("'"); - out.append("\n"); + dump_field(out, "string_array", it, 4); } - out.append("}"); } void ExecuteServiceRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ExecuteServiceRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "ExecuteServiceRequest"); + dump_field(out, "key", this->key); for (const auto &it : this->args) { out.append(" args: "); it.dump_to(out); out.append("\n"); } - out.append("}"); } #endif #ifdef USE_CAMERA void ListEntitiesCameraResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesCameraResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesCameraResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); + dump_field(out, "disabled_by_default", this->disabled_by_default); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "entity_category", static_cast(this->entity_category)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void CameraImageResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("CameraImageResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "CameraImageResponse"); + dump_field(out, "key", this->key); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); out.append("\n"); - - out.append(" done: "); - out.append(YESNO(this->done)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + dump_field(out, "done", this->done); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void CameraImageRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("CameraImageRequest {\n"); - out.append(" single: "); - out.append(YESNO(this->single)); - out.append("\n"); - - out.append(" stream: "); - out.append(YESNO(this->stream)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "CameraImageRequest"); + dump_field(out, "single", this->single); + dump_field(out, "stream", this->stream); } #endif #ifdef USE_CLIMATE void ListEntitiesClimateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesClimateResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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"); - - out.append(" supports_two_point_target_temperature: "); - out.append(YESNO(this->supports_two_point_target_temperature)); - out.append("\n"); - - for (const auto &it : this->supported_modes) { - out.append(" supported_modes: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); + MessageDumpHelper helper(out, "ListEntitiesClimateResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); + dump_field(out, "supports_current_temperature", this->supports_current_temperature); + dump_field(out, "supports_two_point_target_temperature", this->supports_two_point_target_temperature); + for (const auto &it : *this->supported_modes) { + dump_field(out, "supported_modes", static_cast(it), 4); } - - out.append(" visual_min_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->visual_min_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" visual_max_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->visual_max_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" visual_target_temperature_step: "); - snprintf(buffer, sizeof(buffer), "%g", this->visual_target_temperature_step); - 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"); - - for (const auto &it : this->supported_fan_modes) { - out.append(" supported_fan_modes: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); + dump_field(out, "visual_min_temperature", this->visual_min_temperature); + dump_field(out, "visual_max_temperature", this->visual_max_temperature); + dump_field(out, "visual_target_temperature_step", this->visual_target_temperature_step); + dump_field(out, "supports_action", this->supports_action); + for (const auto &it : *this->supported_fan_modes) { + dump_field(out, "supported_fan_modes", static_cast(it), 4); } - - for (const auto &it : this->supported_swing_modes) { - out.append(" supported_swing_modes: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); + for (const auto &it : *this->supported_swing_modes) { + dump_field(out, "supported_swing_modes", static_cast(it), 4); } - - for (const auto &it : this->supported_custom_fan_modes) { - out.append(" supported_custom_fan_modes: "); - out.append("'").append(it).append("'"); - out.append("\n"); + for (const auto &it : *this->supported_custom_fan_modes) { + dump_field(out, "supported_custom_fan_modes", it, 4); } - - for (const auto &it : this->supported_presets) { - out.append(" supported_presets: "); - out.append(proto_enum_to_string(it)); - out.append("\n"); + for (const auto &it : *this->supported_presets) { + dump_field(out, "supported_presets", static_cast(it), 4); } - - for (const auto &it : this->supported_custom_presets) { - out.append(" supported_custom_presets: "); - out.append("'").append(it).append("'"); - out.append("\n"); + for (const auto &it : *this->supported_custom_presets) { + dump_field(out, "supported_custom_presets", it, 4); } - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" visual_current_temperature_step: "); - snprintf(buffer, sizeof(buffer), "%g", this->visual_current_temperature_step); - out.append(buffer); - out.append("\n"); - - out.append(" supports_current_humidity: "); - out.append(YESNO(this->supports_current_humidity)); - out.append("\n"); - - out.append(" supports_target_humidity: "); - out.append(YESNO(this->supports_target_humidity)); - out.append("\n"); - - out.append(" visual_min_humidity: "); - snprintf(buffer, sizeof(buffer), "%g", this->visual_min_humidity); - out.append(buffer); - out.append("\n"); - - out.append(" visual_max_humidity: "); - snprintf(buffer, sizeof(buffer), "%g", this->visual_max_humidity); - out.append(buffer); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + dump_field(out, "disabled_by_default", this->disabled_by_default); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "visual_current_temperature_step", this->visual_current_temperature_step); + dump_field(out, "supports_current_humidity", this->supports_current_humidity); + dump_field(out, "supports_target_humidity", this->supports_target_humidity); + dump_field(out, "visual_min_humidity", this->visual_min_humidity); + dump_field(out, "visual_max_humidity", this->visual_max_humidity); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void ClimateStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ClimateStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - - out.append(" current_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->current_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" target_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" target_temperature_low: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_low); - out.append(buffer); - out.append("\n"); - - out.append(" target_temperature_high: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_high); - 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"); - - out.append(" fan_mode: "); - out.append(proto_enum_to_string(this->fan_mode)); - out.append("\n"); - - out.append(" swing_mode: "); - out.append(proto_enum_to_string(this->swing_mode)); - out.append("\n"); - - out.append(" custom_fan_mode: "); - out.append("'").append(this->custom_fan_mode).append("'"); - out.append("\n"); - - out.append(" preset: "); - out.append(proto_enum_to_string(this->preset)); - out.append("\n"); - - out.append(" custom_preset: "); - out.append("'").append(this->custom_preset).append("'"); - out.append("\n"); - - out.append(" current_humidity: "); - snprintf(buffer, sizeof(buffer), "%g", this->current_humidity); - out.append(buffer); - out.append("\n"); - - out.append(" target_humidity: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_humidity); - out.append(buffer); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ClimateStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, "current_temperature", this->current_temperature); + dump_field(out, "target_temperature", this->target_temperature); + dump_field(out, "target_temperature_low", this->target_temperature_low); + dump_field(out, "target_temperature_high", this->target_temperature_high); + dump_field(out, "action", static_cast(this->action)); + dump_field(out, "fan_mode", static_cast(this->fan_mode)); + dump_field(out, "swing_mode", static_cast(this->swing_mode)); + dump_field(out, "custom_fan_mode", this->custom_fan_mode_ref_); + dump_field(out, "preset", static_cast(this->preset)); + dump_field(out, "custom_preset", this->custom_preset_ref_); + dump_field(out, "current_humidity", this->current_humidity); + dump_field(out, "target_humidity", this->target_humidity); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void ClimateCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ClimateCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_mode: "); - out.append(YESNO(this->has_mode)); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - - out.append(" has_target_temperature: "); - out.append(YESNO(this->has_target_temperature)); - out.append("\n"); - - out.append(" target_temperature: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_temperature); - out.append(buffer); - out.append("\n"); - - out.append(" has_target_temperature_low: "); - out.append(YESNO(this->has_target_temperature_low)); - out.append("\n"); - - out.append(" target_temperature_low: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_low); - out.append(buffer); - out.append("\n"); - - out.append(" has_target_temperature_high: "); - out.append(YESNO(this->has_target_temperature_high)); - out.append("\n"); - - out.append(" target_temperature_high: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_temperature_high); - 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"); - - out.append(" fan_mode: "); - out.append(proto_enum_to_string(this->fan_mode)); - out.append("\n"); - - out.append(" has_swing_mode: "); - out.append(YESNO(this->has_swing_mode)); - out.append("\n"); - - out.append(" swing_mode: "); - out.append(proto_enum_to_string(this->swing_mode)); - out.append("\n"); - - out.append(" has_custom_fan_mode: "); - out.append(YESNO(this->has_custom_fan_mode)); - out.append("\n"); - - out.append(" custom_fan_mode: "); - out.append("'").append(this->custom_fan_mode).append("'"); - out.append("\n"); - - out.append(" has_preset: "); - out.append(YESNO(this->has_preset)); - out.append("\n"); - - out.append(" preset: "); - out.append(proto_enum_to_string(this->preset)); - out.append("\n"); - - out.append(" has_custom_preset: "); - out.append(YESNO(this->has_custom_preset)); - out.append("\n"); - - out.append(" custom_preset: "); - out.append("'").append(this->custom_preset).append("'"); - out.append("\n"); - - out.append(" has_target_humidity: "); - out.append(YESNO(this->has_target_humidity)); - out.append("\n"); - - out.append(" target_humidity: "); - snprintf(buffer, sizeof(buffer), "%g", this->target_humidity); - out.append(buffer); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ClimateCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_mode", this->has_mode); + dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, "has_target_temperature", this->has_target_temperature); + dump_field(out, "target_temperature", this->target_temperature); + dump_field(out, "has_target_temperature_low", this->has_target_temperature_low); + dump_field(out, "target_temperature_low", this->target_temperature_low); + dump_field(out, "has_target_temperature_high", this->has_target_temperature_high); + dump_field(out, "target_temperature_high", this->target_temperature_high); + dump_field(out, "has_fan_mode", this->has_fan_mode); + dump_field(out, "fan_mode", static_cast(this->fan_mode)); + dump_field(out, "has_swing_mode", this->has_swing_mode); + dump_field(out, "swing_mode", static_cast(this->swing_mode)); + dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode); + dump_field(out, "custom_fan_mode", this->custom_fan_mode); + dump_field(out, "has_preset", this->has_preset); + dump_field(out, "preset", static_cast(this->preset)); + dump_field(out, "has_custom_preset", this->has_custom_preset); + dump_field(out, "custom_preset", this->custom_preset); + dump_field(out, "has_target_humidity", this->has_target_humidity); + dump_field(out, "target_humidity", this->target_humidity); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_NUMBER void ListEntitiesNumberResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesNumberResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" min_value: "); - snprintf(buffer, sizeof(buffer), "%g", this->min_value); - out.append(buffer); - out.append("\n"); - - out.append(" max_value: "); - snprintf(buffer, sizeof(buffer), "%g", this->max_value); - out.append(buffer); - out.append("\n"); - - out.append(" step: "); - snprintf(buffer, sizeof(buffer), "%g", this->step); - out.append(buffer); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" unit_of_measurement: "); - out.append("'").append(this->unit_of_measurement).append("'"); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesNumberResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "min_value", this->min_value); + dump_field(out, "max_value", this->max_value); + dump_field(out, "step", this->step); + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "unit_of_measurement", this->unit_of_measurement_ref_); + dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, "device_class", this->device_class_ref_); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void NumberStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("NumberStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - snprintf(buffer, sizeof(buffer), "%g", this->state); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "NumberStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); + dump_field(out, "missing_state", this->missing_state); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void NumberCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("NumberCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - snprintf(buffer, sizeof(buffer), "%g", this->state); - out.append(buffer); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "NumberCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_SELECT void ListEntitiesSelectResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesSelectResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - for (const auto &it : this->options) { - out.append(" options: "); - out.append("'").append(it).append("'"); - out.append("\n"); + MessageDumpHelper helper(out, "ListEntitiesSelectResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + for (const auto &it : *this->options) { + dump_field(out, "options", it, 4); } - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void SelectStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SelectStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SelectStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state_ref_); + dump_field(out, "missing_state", this->missing_state); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void SelectCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SelectCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SelectCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_SIREN void ListEntitiesSirenResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesSirenResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesSirenResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); for (const auto &it : this->tones) { - out.append(" tones: "); - out.append("'").append(it).append("'"); - out.append("\n"); + dump_field(out, "tones", it, 4); } - - out.append(" supports_duration: "); - out.append(YESNO(this->supports_duration)); - out.append("\n"); - - out.append(" supports_volume: "); - out.append(YESNO(this->supports_volume)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + dump_field(out, "supports_duration", this->supports_duration); + dump_field(out, "supports_volume", this->supports_volume); + dump_field(out, "entity_category", static_cast(this->entity_category)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void SirenStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SirenStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SirenStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void SirenCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SirenCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_state: "); - out.append(YESNO(this->has_state)); - out.append("\n"); - - out.append(" state: "); - out.append(YESNO(this->state)); - out.append("\n"); - - out.append(" has_tone: "); - out.append(YESNO(this->has_tone)); - out.append("\n"); - - out.append(" tone: "); - out.append("'").append(this->tone).append("'"); - out.append("\n"); - - out.append(" has_duration: "); - out.append(YESNO(this->has_duration)); - out.append("\n"); - - out.append(" duration: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->duration); - out.append(buffer); - out.append("\n"); - - out.append(" has_volume: "); - out.append(YESNO(this->has_volume)); - out.append("\n"); - - out.append(" volume: "); - snprintf(buffer, sizeof(buffer), "%g", this->volume); - out.append(buffer); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SirenCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_state", this->has_state); + dump_field(out, "state", this->state); + dump_field(out, "has_tone", this->has_tone); + dump_field(out, "tone", this->tone); + dump_field(out, "has_duration", this->has_duration); + dump_field(out, "duration", this->duration); + dump_field(out, "has_volume", this->has_volume); + dump_field(out, "volume", this->volume); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_LOCK void ListEntitiesLockResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesLockResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" assumed_state: "); - out.append(YESNO(this->assumed_state)); - out.append("\n"); - - out.append(" supports_open: "); - out.append(YESNO(this->supports_open)); - out.append("\n"); - - out.append(" requires_code: "); - out.append(YESNO(this->requires_code)); - out.append("\n"); - - out.append(" code_format: "); - out.append("'").append(this->code_format).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesLockResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "assumed_state", this->assumed_state); + dump_field(out, "supports_open", this->supports_open); + dump_field(out, "requires_code", this->requires_code); + dump_field(out, "code_format", this->code_format_ref_); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void LockStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("LockStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(proto_enum_to_string(this->state)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "LockStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", static_cast(this->state)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void LockCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("LockCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" command: "); - out.append(proto_enum_to_string(this->command)); - out.append("\n"); - - out.append(" has_code: "); - out.append(YESNO(this->has_code)); - out.append("\n"); - - out.append(" code: "); - out.append("'").append(this->code).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "LockCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "command", static_cast(this->command)); + dump_field(out, "has_code", this->has_code); + dump_field(out, "code", this->code); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_BUTTON void ListEntitiesButtonResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesButtonResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesButtonResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "device_class", this->device_class_ref_); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void ButtonCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ButtonCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ButtonCommandRequest"); + dump_field(out, "key", this->key); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_MEDIA_PLAYER void MediaPlayerSupportedFormat::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("MediaPlayerSupportedFormat {\n"); - out.append(" format: "); - out.append("'").append(this->format).append("'"); - out.append("\n"); - - out.append(" sample_rate: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->sample_rate); - out.append(buffer); - out.append("\n"); - - out.append(" num_channels: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->num_channels); - out.append(buffer); - out.append("\n"); - - out.append(" purpose: "); - out.append(proto_enum_to_string(this->purpose)); - out.append("\n"); - - out.append(" sample_bytes: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->sample_bytes); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "MediaPlayerSupportedFormat"); + dump_field(out, "format", this->format_ref_); + dump_field(out, "sample_rate", this->sample_rate); + dump_field(out, "num_channels", this->num_channels); + dump_field(out, "purpose", static_cast(this->purpose)); + dump_field(out, "sample_bytes", this->sample_bytes); } void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesMediaPlayerResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" supports_pause: "); - out.append(YESNO(this->supports_pause)); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesMediaPlayerResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "supports_pause", this->supports_pause); for (const auto &it : this->supported_formats) { out.append(" supported_formats: "); it.dump_to(out); out.append("\n"); } - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif + dump_field(out, "feature_flags", this->feature_flags); } void MediaPlayerStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("MediaPlayerStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(proto_enum_to_string(this->state)); - out.append("\n"); - - out.append(" volume: "); - snprintf(buffer, sizeof(buffer), "%g", this->volume); - out.append(buffer); - out.append("\n"); - - out.append(" muted: "); - out.append(YESNO(this->muted)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "MediaPlayerStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", static_cast(this->state)); + dump_field(out, "volume", this->volume); + dump_field(out, "muted", this->muted); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void MediaPlayerCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("MediaPlayerCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_command: "); - out.append(YESNO(this->has_command)); - out.append("\n"); - - out.append(" command: "); - out.append(proto_enum_to_string(this->command)); - out.append("\n"); - - out.append(" has_volume: "); - out.append(YESNO(this->has_volume)); - out.append("\n"); - - out.append(" volume: "); - snprintf(buffer, sizeof(buffer), "%g", this->volume); - out.append(buffer); - out.append("\n"); - - out.append(" has_media_url: "); - out.append(YESNO(this->has_media_url)); - out.append("\n"); - - out.append(" media_url: "); - out.append("'").append(this->media_url).append("'"); - out.append("\n"); - - out.append(" has_announcement: "); - out.append(YESNO(this->has_announcement)); - out.append("\n"); - - out.append(" announcement: "); - out.append(YESNO(this->announcement)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "MediaPlayerCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_command", this->has_command); + dump_field(out, "command", static_cast(this->command)); + dump_field(out, "has_volume", this->has_volume); + dump_field(out, "volume", this->volume); + dump_field(out, "has_media_url", this->has_media_url); + dump_field(out, "media_url", this->media_url); + dump_field(out, "has_announcement", this->has_announcement); + dump_field(out, "announcement", this->announcement); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_BLUETOOTH_PROXY void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeBluetoothLEAdvertisementsRequest {\n"); - out.append(" flags: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); - out.append(buffer); - 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("}"); + MessageDumpHelper helper(out, "SubscribeBluetoothLEAdvertisementsRequest"); + dump_field(out, "flags", this->flags); } void BluetoothLERawAdvertisement::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothLERawAdvertisement {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" rssi: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->rssi); - out.append(buffer); - out.append("\n"); - - out.append(" address_type: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothLERawAdvertisement"); + dump_field(out, "address", this->address); + dump_field(out, "rssi", this->rssi); + dump_field(out, "address_type", this->address_type); 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("}"); } void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothLERawAdvertisementsResponse {\n"); - for (const auto &it : this->advertisements) { + MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse"); + for (uint16_t i = 0; i < this->advertisements_len; i++) { out.append(" advertisements: "); - it.dump_to(out); + this->advertisements[i].dump_to(out); out.append("\n"); } - out.append("}"); } void BluetoothDeviceRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDeviceRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" request_type: "); - out.append(proto_enum_to_string(this->request_type)); - out.append("\n"); - - out.append(" has_address_type: "); - out.append(YESNO(this->has_address_type)); - out.append("\n"); - - out.append(" address_type: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->address_type); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothDeviceRequest"); + dump_field(out, "address", this->address); + dump_field(out, "request_type", static_cast(this->request_type)); + dump_field(out, "has_address_type", this->has_address_type); + dump_field(out, "address_type", this->address_type); } void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDeviceConnectionResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" connected: "); - out.append(YESNO(this->connected)); - out.append("\n"); - - out.append(" mtu: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->mtu); - out.append(buffer); - out.append("\n"); - - out.append(" error: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); -} -void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTGetServicesRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothDeviceConnectionResponse"); + dump_field(out, "address", this->address); + dump_field(out, "connected", this->connected); + dump_field(out, "mtu", this->mtu); + dump_field(out, "error", this->error); } +void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { dump_field(out, "address", this->address); } void BluetoothGATTDescriptor::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTDescriptor {\n"); + MessageDumpHelper helper(out, "BluetoothGATTDescriptor"); for (const auto &it : this->uuid) { - out.append(" uuid: "); - snprintf(buffer, sizeof(buffer), "%llu", it); - out.append(buffer); - out.append("\n"); + dump_field(out, "uuid", it, 4); } - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); + dump_field(out, "handle", this->handle); + dump_field(out, "short_uuid", this->short_uuid); } void BluetoothGATTCharacteristic::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTCharacteristic {\n"); + MessageDumpHelper helper(out, "BluetoothGATTCharacteristic"); for (const auto &it : this->uuid) { - out.append(" uuid: "); - snprintf(buffer, sizeof(buffer), "%llu", it); - out.append(buffer); - out.append("\n"); + dump_field(out, "uuid", it, 4); } - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" properties: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->properties); - out.append(buffer); - out.append("\n"); - + dump_field(out, "handle", this->handle); + dump_field(out, "properties", this->properties); for (const auto &it : this->descriptors) { out.append(" descriptors: "); it.dump_to(out); out.append("\n"); } - out.append("}"); + dump_field(out, "short_uuid", this->short_uuid); } void BluetoothGATTService::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTService {\n"); + MessageDumpHelper helper(out, "BluetoothGATTService"); for (const auto &it : this->uuid) { - out.append(" uuid: "); - snprintf(buffer, sizeof(buffer), "%llu", it); - out.append(buffer); - out.append("\n"); + dump_field(out, "uuid", it, 4); } - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - + dump_field(out, "handle", this->handle); for (const auto &it : this->characteristics) { out.append(" characteristics: "); it.dump_to(out); out.append("\n"); } - out.append("}"); + dump_field(out, "short_uuid", this->short_uuid); } void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTGetServicesResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse"); + dump_field(out, "address", this->address); for (const auto &it : this->services) { out.append(" services: "); it.dump_to(out); out.append("\n"); } - out.append("}"); } void BluetoothGATTGetServicesDoneResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTGetServicesDoneResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTGetServicesDoneResponse"); + dump_field(out, "address", this->address); } void BluetoothGATTReadRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTReadRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTReadRequest"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); } void BluetoothGATTReadResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTReadResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothGATTReadResponse"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); out.append("\n"); - out.append("}"); } void BluetoothGATTWriteRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTWriteRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" response: "); - out.append(YESNO(this->response)); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothGATTWriteRequest"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); + dump_field(out, "response", this->response); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + out.append(format_hex_pretty(reinterpret_cast(this->data.data()), this->data.size())); out.append("\n"); - out.append("}"); } void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTReadDescriptorRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTReadDescriptorRequest"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); } void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTWriteDescriptorRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothGATTWriteDescriptorRequest"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + out.append(format_hex_pretty(reinterpret_cast(this->data.data()), this->data.size())); out.append("\n"); - out.append("}"); } void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTNotifyRequest {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" enable: "); - out.append(YESNO(this->enable)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTNotifyRequest"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); + dump_field(out, "enable", this->enable); } void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTNotifyDataResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothGATTNotifyDataResponse"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); out.append("\n"); - out.append("}"); } void SubscribeBluetoothConnectionsFreeRequest::dump_to(std::string &out) const { out.append("SubscribeBluetoothConnectionsFreeRequest {}"); } void BluetoothConnectionsFreeResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothConnectionsFreeResponse {\n"); - out.append(" free: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->free); - out.append(buffer); - out.append("\n"); - - out.append(" limit: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->limit); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse"); + dump_field(out, "free", this->free); + dump_field(out, "limit", this->limit); for (const auto &it : this->allocated) { - out.append(" allocated: "); - snprintf(buffer, sizeof(buffer), "%llu", it); - out.append(buffer); - out.append("\n"); + dump_field(out, "allocated", it, 4); } - out.append("}"); } void BluetoothGATTErrorResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTErrorResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - - out.append(" error: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTErrorResponse"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); + dump_field(out, "error", this->error); } void BluetoothGATTWriteResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTWriteResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTWriteResponse"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); } void BluetoothGATTNotifyResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothGATTNotifyResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" handle: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->handle); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothGATTNotifyResponse"); + dump_field(out, "address", this->address); + dump_field(out, "handle", this->handle); } void BluetoothDevicePairingResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDevicePairingResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" paired: "); - out.append(YESNO(this->paired)); - out.append("\n"); - - out.append(" error: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothDevicePairingResponse"); + dump_field(out, "address", this->address); + dump_field(out, "paired", this->paired); + dump_field(out, "error", this->error); } void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDeviceUnpairingResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" success: "); - out.append(YESNO(this->success)); - out.append("\n"); - - out.append(" error: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothDeviceUnpairingResponse"); + dump_field(out, "address", this->address); + dump_field(out, "success", this->success); + dump_field(out, "error", this->error); } void UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}"); } void BluetoothDeviceClearCacheResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothDeviceClearCacheResponse {\n"); - out.append(" address: "); - snprintf(buffer, sizeof(buffer), "%llu", this->address); - out.append(buffer); - out.append("\n"); - - out.append(" success: "); - out.append(YESNO(this->success)); - out.append("\n"); - - out.append(" error: "); - snprintf(buffer, sizeof(buffer), "%" PRId32, this->error); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse"); + dump_field(out, "address", this->address); + dump_field(out, "success", this->success); + dump_field(out, "error", this->error); } void BluetoothScannerStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothScannerStateResponse {\n"); - out.append(" state: "); - out.append(proto_enum_to_string(this->state)); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothScannerStateResponse"); + dump_field(out, "state", static_cast(this->state)); + dump_field(out, "mode", static_cast(this->mode)); } void BluetoothScannerSetModeRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("BluetoothScannerSetModeRequest {\n"); - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest"); + dump_field(out, "mode", static_cast(this->mode)); } #endif #ifdef USE_VOICE_ASSISTANT void SubscribeVoiceAssistantRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("SubscribeVoiceAssistantRequest {\n"); - out.append(" subscribe: "); - out.append(YESNO(this->subscribe)); - out.append("\n"); - - out.append(" flags: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "SubscribeVoiceAssistantRequest"); + dump_field(out, "subscribe", this->subscribe); + dump_field(out, "flags", this->flags); } void VoiceAssistantAudioSettings::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantAudioSettings {\n"); - out.append(" noise_suppression_level: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->noise_suppression_level); - out.append(buffer); - out.append("\n"); - - out.append(" auto_gain: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->auto_gain); - out.append(buffer); - out.append("\n"); - - out.append(" volume_multiplier: "); - snprintf(buffer, sizeof(buffer), "%g", this->volume_multiplier); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "VoiceAssistantAudioSettings"); + dump_field(out, "noise_suppression_level", this->noise_suppression_level); + dump_field(out, "auto_gain", this->auto_gain); + dump_field(out, "volume_multiplier", this->volume_multiplier); } void VoiceAssistantRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantRequest {\n"); - out.append(" start: "); - out.append(YESNO(this->start)); - out.append("\n"); - - out.append(" conversation_id: "); - out.append("'").append(this->conversation_id).append("'"); - out.append("\n"); - - out.append(" flags: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->flags); - out.append(buffer); - out.append("\n"); - + MessageDumpHelper helper(out, "VoiceAssistantRequest"); + dump_field(out, "start", this->start); + dump_field(out, "conversation_id", this->conversation_id_ref_); + dump_field(out, "flags", this->flags); out.append(" audio_settings: "); this->audio_settings.dump_to(out); out.append("\n"); - - out.append(" wake_word_phrase: "); - out.append("'").append(this->wake_word_phrase).append("'"); - out.append("\n"); - out.append("}"); + dump_field(out, "wake_word_phrase", this->wake_word_phrase_ref_); } void VoiceAssistantResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantResponse {\n"); - out.append(" port: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->port); - out.append(buffer); - out.append("\n"); - - out.append(" error: "); - out.append(YESNO(this->error)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "VoiceAssistantResponse"); + dump_field(out, "port", this->port); + dump_field(out, "error", this->error); } void VoiceAssistantEventData::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantEventData {\n"); - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" value: "); - out.append("'").append(this->value).append("'"); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "VoiceAssistantEventData"); + dump_field(out, "name", this->name); + dump_field(out, "value", this->value); } void VoiceAssistantEventResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantEventResponse {\n"); - out.append(" event_type: "); - out.append(proto_enum_to_string(this->event_type)); - out.append("\n"); - + MessageDumpHelper helper(out, "VoiceAssistantEventResponse"); + dump_field(out, "event_type", static_cast(this->event_type)); for (const auto &it : this->data) { out.append(" data: "); it.dump_to(out); out.append("\n"); } - out.append("}"); } void VoiceAssistantAudio::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantAudio {\n"); + MessageDumpHelper helper(out, "VoiceAssistantAudio"); out.append(" data: "); - out.append(format_hex_pretty(this->data)); + if (this->data_ptr_ != nullptr) { + out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); + } else { + out.append(format_hex_pretty(reinterpret_cast(this->data.data()), this->data.size())); + } out.append("\n"); - - out.append(" end: "); - out.append(YESNO(this->end)); - out.append("\n"); - out.append("}"); + dump_field(out, "end", this->end); } void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantTimerEventResponse {\n"); - out.append(" event_type: "); - out.append(proto_enum_to_string(this->event_type)); - out.append("\n"); - - out.append(" timer_id: "); - out.append("'").append(this->timer_id).append("'"); - out.append("\n"); - - out.append(" name: "); - out.append("'").append(this->name).append("'"); - out.append("\n"); - - out.append(" total_seconds: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->total_seconds); - out.append(buffer); - out.append("\n"); - - out.append(" seconds_left: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->seconds_left); - out.append(buffer); - out.append("\n"); - - out.append(" is_active: "); - out.append(YESNO(this->is_active)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "VoiceAssistantTimerEventResponse"); + dump_field(out, "event_type", static_cast(this->event_type)); + dump_field(out, "timer_id", this->timer_id); + dump_field(out, "name", this->name); + dump_field(out, "total_seconds", this->total_seconds); + dump_field(out, "seconds_left", this->seconds_left); + dump_field(out, "is_active", this->is_active); } void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantAnnounceRequest {\n"); - out.append(" media_id: "); - out.append("'").append(this->media_id).append("'"); - out.append("\n"); - - out.append(" text: "); - out.append("'").append(this->text).append("'"); - out.append("\n"); - - out.append(" preannounce_media_id: "); - out.append("'").append(this->preannounce_media_id).append("'"); - out.append("\n"); - - out.append(" start_conversation: "); - out.append(YESNO(this->start_conversation)); - out.append("\n"); - out.append("}"); -} -void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantAnnounceFinished {\n"); - out.append(" success: "); - out.append(YESNO(this->success)); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "VoiceAssistantAnnounceRequest"); + dump_field(out, "media_id", this->media_id); + dump_field(out, "text", this->text); + dump_field(out, "preannounce_media_id", this->preannounce_media_id); + dump_field(out, "start_conversation", this->start_conversation); } +void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { dump_field(out, "success", this->success); } void VoiceAssistantWakeWord::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantWakeWord {\n"); - out.append(" id: "); - out.append("'").append(this->id).append("'"); - out.append("\n"); - - out.append(" wake_word: "); - out.append("'").append(this->wake_word).append("'"); - out.append("\n"); - + MessageDumpHelper helper(out, "VoiceAssistantWakeWord"); + dump_field(out, "id", this->id_ref_); + dump_field(out, "wake_word", this->wake_word_ref_); for (const auto &it : this->trained_languages) { - out.append(" trained_languages: "); - out.append("'").append(it).append("'"); - out.append("\n"); + dump_field(out, "trained_languages", it, 4); } - out.append("}"); } void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { out.append("VoiceAssistantConfigurationRequest {}"); } void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantConfigurationResponse {\n"); + MessageDumpHelper helper(out, "VoiceAssistantConfigurationResponse"); for (const auto &it : this->available_wake_words) { out.append(" available_wake_words: "); it.dump_to(out); out.append("\n"); } - - for (const auto &it : this->active_wake_words) { - out.append(" active_wake_words: "); - out.append("'").append(it).append("'"); - out.append("\n"); + for (const auto &it : *this->active_wake_words) { + dump_field(out, "active_wake_words", it, 4); } - - out.append(" max_active_wake_words: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->max_active_wake_words); - out.append(buffer); - out.append("\n"); - out.append("}"); + dump_field(out, "max_active_wake_words", this->max_active_wake_words); } void VoiceAssistantSetConfiguration::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("VoiceAssistantSetConfiguration {\n"); + MessageDumpHelper helper(out, "VoiceAssistantSetConfiguration"); for (const auto &it : this->active_wake_words) { - out.append(" active_wake_words: "); - out.append("'").append(it).append("'"); - out.append("\n"); + dump_field(out, "active_wake_words", it, 4); } - out.append("}"); } #endif #ifdef USE_ALARM_CONTROL_PANEL void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesAlarmControlPanelResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" supported_features: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->supported_features); - out.append(buffer); - out.append("\n"); - - out.append(" requires_code: "); - out.append(YESNO(this->requires_code)); - out.append("\n"); - - out.append(" requires_code_to_arm: "); - out.append(YESNO(this->requires_code_to_arm)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesAlarmControlPanelResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "supported_features", this->supported_features); + dump_field(out, "requires_code", this->requires_code); + dump_field(out, "requires_code_to_arm", this->requires_code_to_arm); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void AlarmControlPanelStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("AlarmControlPanelStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append(proto_enum_to_string(this->state)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "AlarmControlPanelStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", static_cast(this->state)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("AlarmControlPanelCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" command: "); - out.append(proto_enum_to_string(this->command)); - out.append("\n"); - - out.append(" code: "); - out.append("'").append(this->code).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "AlarmControlPanelCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "command", static_cast(this->command)); + dump_field(out, "code", this->code); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_TEXT void ListEntitiesTextResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesTextResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" min_length: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->min_length); - out.append(buffer); - out.append("\n"); - - out.append(" max_length: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->max_length); - out.append(buffer); - out.append("\n"); - - out.append(" pattern: "); - out.append("'").append(this->pattern).append("'"); - out.append("\n"); - - out.append(" mode: "); - out.append(proto_enum_to_string(this->mode)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesTextResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "min_length", this->min_length); + dump_field(out, "max_length", this->max_length); + dump_field(out, "pattern", this->pattern_ref_); + dump_field(out, "mode", static_cast(this->mode)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void TextStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TextStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "TextStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state_ref_); + dump_field(out, "missing_state", this->missing_state); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void TextCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TextCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" state: "); - out.append("'").append(this->state).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "TextCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "state", this->state); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_DATETIME_DATE void ListEntitiesDateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesDateResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesDateResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void DateStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DateStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" year: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->year); - out.append(buffer); - out.append("\n"); - - out.append(" month: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->month); - out.append(buffer); - out.append("\n"); - - out.append(" day: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day); - out.append(buffer); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "DateStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "missing_state", this->missing_state); + dump_field(out, "year", this->year); + dump_field(out, "month", this->month); + dump_field(out, "day", this->day); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void DateCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DateCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" year: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->year); - out.append(buffer); - out.append("\n"); - - out.append(" month: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->month); - out.append(buffer); - out.append("\n"); - - out.append(" day: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->day); - out.append(buffer); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "DateCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "year", this->year); + dump_field(out, "month", this->month); + dump_field(out, "day", this->day); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_DATETIME_TIME void ListEntitiesTimeResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesTimeResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesTimeResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void TimeStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TimeStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" hour: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->hour); - out.append(buffer); - out.append("\n"); - - out.append(" minute: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->minute); - out.append(buffer); - out.append("\n"); - - out.append(" second: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second); - out.append(buffer); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "TimeStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "missing_state", this->missing_state); + dump_field(out, "hour", this->hour); + dump_field(out, "minute", this->minute); + dump_field(out, "second", this->second); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void TimeCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("TimeCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" hour: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->hour); - out.append(buffer); - out.append("\n"); - - out.append(" minute: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->minute); - out.append(buffer); - out.append("\n"); - - out.append(" second: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->second); - out.append(buffer); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "TimeCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "hour", this->hour); + dump_field(out, "minute", this->minute); + dump_field(out, "second", this->second); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_EVENT void ListEntitiesEventResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesEventResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - + MessageDumpHelper helper(out, "ListEntitiesEventResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "device_class", this->device_class_ref_); for (const auto &it : this->event_types) { - out.append(" event_types: "); - out.append("'").append(it).append("'"); - out.append("\n"); + dump_field(out, "event_types", it, 4); } - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void EventResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("EventResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" event_type: "); - out.append("'").append(this->event_type).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "EventResponse"); + dump_field(out, "key", this->key); + dump_field(out, "event_type", this->event_type_ref_); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_VALVE void ListEntitiesValveResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesValveResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - out.append(" assumed_state: "); - out.append(YESNO(this->assumed_state)); - out.append("\n"); - - out.append(" supports_position: "); - out.append(YESNO(this->supports_position)); - out.append("\n"); - - out.append(" supports_stop: "); - out.append(YESNO(this->supports_stop)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesValveResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "assumed_state", this->assumed_state); + dump_field(out, "supports_position", this->supports_position); + dump_field(out, "supports_stop", this->supports_stop); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void ValveStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ValveStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" position: "); - snprintf(buffer, sizeof(buffer), "%g", this->position); - out.append(buffer); - out.append("\n"); - - out.append(" current_operation: "); - out.append(proto_enum_to_string(this->current_operation)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ValveStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "position", this->position); + dump_field(out, "current_operation", static_cast(this->current_operation)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void ValveCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ValveCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" has_position: "); - out.append(YESNO(this->has_position)); - out.append("\n"); - - out.append(" position: "); - snprintf(buffer, sizeof(buffer), "%g", this->position); - out.append(buffer); - out.append("\n"); - - out.append(" stop: "); - out.append(YESNO(this->stop)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ValveCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_position", this->has_position); + dump_field(out, "position", this->position); + dump_field(out, "stop", this->stop); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_DATETIME_DATETIME void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesDateTimeResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesDateTimeResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void DateTimeStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DateTimeStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" epoch_seconds: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); - out.append(buffer); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "DateTimeStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "missing_state", this->missing_state); + dump_field(out, "epoch_seconds", this->epoch_seconds); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void DateTimeCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("DateTimeCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" epoch_seconds: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->epoch_seconds); - out.append(buffer); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "DateTimeCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "epoch_seconds", this->epoch_seconds); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif #ifdef USE_UPDATE void ListEntitiesUpdateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("ListEntitiesUpdateResponse {\n"); - out.append(" object_id: "); - out.append("'").append(this->object_id).append("'"); - out.append("\n"); - - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" name: "); - 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(" icon: "); - out.append("'").append(this->icon).append("'"); - out.append("\n"); - - out.append(" disabled_by_default: "); - out.append(YESNO(this->disabled_by_default)); - out.append("\n"); - - out.append(" entity_category: "); - out.append(proto_enum_to_string(this->entity_category)); - out.append("\n"); - - out.append(" device_class: "); - out.append("'").append(this->device_class).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "ListEntitiesUpdateResponse"); + dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name_ref_); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon_ref_); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, "device_class", this->device_class_ref_); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void UpdateStateResponse::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("UpdateStateResponse {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" missing_state: "); - out.append(YESNO(this->missing_state)); - out.append("\n"); - - out.append(" in_progress: "); - out.append(YESNO(this->in_progress)); - out.append("\n"); - - out.append(" has_progress: "); - out.append(YESNO(this->has_progress)); - out.append("\n"); - - out.append(" progress: "); - snprintf(buffer, sizeof(buffer), "%g", this->progress); - out.append(buffer); - out.append("\n"); - - out.append(" current_version: "); - out.append("'").append(this->current_version).append("'"); - out.append("\n"); - - out.append(" latest_version: "); - out.append("'").append(this->latest_version).append("'"); - out.append("\n"); - - out.append(" title: "); - out.append("'").append(this->title).append("'"); - out.append("\n"); - - out.append(" release_summary: "); - out.append("'").append(this->release_summary).append("'"); - out.append("\n"); - - out.append(" release_url: "); - out.append("'").append(this->release_url).append("'"); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "UpdateStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "missing_state", this->missing_state); + dump_field(out, "in_progress", this->in_progress); + dump_field(out, "has_progress", this->has_progress); + dump_field(out, "progress", this->progress); + dump_field(out, "current_version", this->current_version_ref_); + dump_field(out, "latest_version", this->latest_version_ref_); + dump_field(out, "title", this->title_ref_); + dump_field(out, "release_summary", this->release_summary_ref_); + dump_field(out, "release_url", this->release_url_ref_); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } void UpdateCommandRequest::dump_to(std::string &out) const { - __attribute__((unused)) char buffer[64]; - out.append("UpdateCommandRequest {\n"); - out.append(" key: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->key); - out.append(buffer); - out.append("\n"); - - out.append(" command: "); - out.append(proto_enum_to_string(this->command)); - out.append("\n"); - - out.append(" device_id: "); - snprintf(buffer, sizeof(buffer), "%" PRIu32, this->device_id); - out.append(buffer); - out.append("\n"); - out.append("}"); + MessageDumpHelper helper(out, "UpdateCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "command", static_cast(this->command)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif } #endif -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif // HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_includes.h b/esphome/components/api/api_pb2_includes.h new file mode 100644 index 0000000000..55d95304b1 --- /dev/null +++ b/esphome/components/api/api_pb2_includes.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/core/defines.h" + +// This file provides includes needed by the generated protobuf code +// when using pointer optimizations for component-specific types + +#ifdef USE_CLIMATE +#include "esphome/components/climate/climate_mode.h" +#include "esphome/components/climate/climate_traits.h" +#endif + +#ifdef USE_LIGHT +#include "esphome/components/light/light_traits.h" +#endif + +#ifdef USE_FAN +#include "esphome/components/fan/fan_traits.h" +#endif + +#ifdef USE_SELECT +#include "esphome/components/select/select_traits.h" +#endif + +// Standard library includes that might be needed +#include +#include +#include + +namespace esphome::api { + +// This file only provides includes, no actual code + +} // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index b96e5736a4..6b7b8b9ebd 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -3,8 +3,7 @@ #include "api_pb2_service.h" #include "esphome/core/log.h" -namespace esphome { -namespace api { +namespace esphome::api { static const char *const TAG = "api.service"; @@ -16,7 +15,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { - case 1: { + case HelloRequest::MESSAGE_TYPE: { HelloRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -25,7 +24,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_hello_request(msg); break; } - case 3: { + case ConnectRequest::MESSAGE_TYPE: { ConnectRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -34,70 +33,70 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_connect_request(msg); break; } - case 5: { + case DisconnectRequest::MESSAGE_TYPE: { DisconnectRequest msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str()); #endif this->on_disconnect_request(msg); break; } - case 6: { + case DisconnectResponse::MESSAGE_TYPE: { DisconnectResponse msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str()); #endif this->on_disconnect_response(msg); break; } - case 7: { + case PingRequest::MESSAGE_TYPE: { PingRequest msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str()); #endif this->on_ping_request(msg); break; } - case 8: { + case PingResponse::MESSAGE_TYPE: { PingResponse msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str()); #endif this->on_ping_response(msg); break; } - case 9: { + case DeviceInfoRequest::MESSAGE_TYPE: { DeviceInfoRequest msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str()); #endif this->on_device_info_request(msg); break; } - case 11: { + case ListEntitiesRequest::MESSAGE_TYPE: { ListEntitiesRequest msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str()); #endif this->on_list_entities_request(msg); break; } - case 20: { + case SubscribeStatesRequest::MESSAGE_TYPE: { SubscribeStatesRequest msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str()); #endif this->on_subscribe_states_request(msg); break; } - case 28: { + case SubscribeLogsRequest::MESSAGE_TYPE: { SubscribeLogsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -107,7 +106,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #ifdef USE_COVER - case 30: { + case CoverCommandRequest::MESSAGE_TYPE: { CoverCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -118,7 +117,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_FAN - case 31: { + case FanCommandRequest::MESSAGE_TYPE: { FanCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -129,7 +128,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_LIGHT - case 32: { + case LightCommandRequest::MESSAGE_TYPE: { LightCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -140,7 +139,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_SWITCH - case 33: { + case SwitchCommandRequest::MESSAGE_TYPE: { SwitchCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -150,25 +149,27 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #endif - case 34: { +#ifdef USE_API_HOMEASSISTANT_SERVICES + case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: { SubscribeHomeassistantServicesRequest msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str()); #endif this->on_subscribe_homeassistant_services_request(msg); break; } - case 36: { +#endif + case GetTimeRequest::MESSAGE_TYPE: { GetTimeRequest msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str()); #endif this->on_get_time_request(msg); break; } - case 37: { + case GetTimeResponse::MESSAGE_TYPE: { GetTimeResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -177,16 +178,19 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_get_time_response(msg); break; } - case 38: { +#ifdef USE_API_HOMEASSISTANT_STATES + case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: { SubscribeHomeAssistantStatesRequest msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str()); #endif this->on_subscribe_home_assistant_states_request(msg); break; } - case 40: { +#endif +#ifdef USE_API_HOMEASSISTANT_STATES + case HomeAssistantStateResponse::MESSAGE_TYPE: { HomeAssistantStateResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -195,8 +199,9 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_home_assistant_state_response(msg); break; } +#endif #ifdef USE_API_SERVICES - case 42: { + case ExecuteServiceRequest::MESSAGE_TYPE: { ExecuteServiceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -207,7 +212,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_CAMERA - case 45: { + case CameraImageRequest::MESSAGE_TYPE: { CameraImageRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -218,7 +223,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_CLIMATE - case 48: { + case ClimateCommandRequest::MESSAGE_TYPE: { ClimateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -229,7 +234,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_NUMBER - case 51: { + case NumberCommandRequest::MESSAGE_TYPE: { NumberCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -240,7 +245,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_SELECT - case 54: { + case SelectCommandRequest::MESSAGE_TYPE: { SelectCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -251,7 +256,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_SIREN - case 57: { + case SirenCommandRequest::MESSAGE_TYPE: { SirenCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -262,7 +267,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_LOCK - case 60: { + case LockCommandRequest::MESSAGE_TYPE: { LockCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -273,7 +278,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BUTTON - case 62: { + case ButtonCommandRequest::MESSAGE_TYPE: { ButtonCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -284,7 +289,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_MEDIA_PLAYER - case 65: { + case MediaPlayerCommandRequest::MESSAGE_TYPE: { MediaPlayerCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -295,7 +300,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 66: { + case SubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { SubscribeBluetoothLEAdvertisementsRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -306,7 +311,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 68: { + case BluetoothDeviceRequest::MESSAGE_TYPE: { BluetoothDeviceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -317,7 +322,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 70: { + case BluetoothGATTGetServicesRequest::MESSAGE_TYPE: { BluetoothGATTGetServicesRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -328,7 +333,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 73: { + case BluetoothGATTReadRequest::MESSAGE_TYPE: { BluetoothGATTReadRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -339,7 +344,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 75: { + case BluetoothGATTWriteRequest::MESSAGE_TYPE: { BluetoothGATTWriteRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -350,7 +355,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 76: { + case BluetoothGATTReadDescriptorRequest::MESSAGE_TYPE: { BluetoothGATTReadDescriptorRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -361,7 +366,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 77: { + case BluetoothGATTWriteDescriptorRequest::MESSAGE_TYPE: { BluetoothGATTWriteDescriptorRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -372,7 +377,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 78: { + case BluetoothGATTNotifyRequest::MESSAGE_TYPE: { BluetoothGATTNotifyRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -383,9 +388,9 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 80: { + case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: { SubscribeBluetoothConnectionsFreeRequest msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_subscribe_bluetooth_connections_free_request: %s", msg.dump().c_str()); #endif @@ -394,9 +399,9 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 87: { + case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { UnsubscribeBluetoothLEAdvertisementsRequest msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_unsubscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str()); #endif @@ -405,7 +410,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 89: { + case SubscribeVoiceAssistantRequest::MESSAGE_TYPE: { SubscribeVoiceAssistantRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -416,7 +421,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 91: { + case VoiceAssistantResponse::MESSAGE_TYPE: { VoiceAssistantResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -427,7 +432,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 92: { + case VoiceAssistantEventResponse::MESSAGE_TYPE: { VoiceAssistantEventResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -438,7 +443,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_ALARM_CONTROL_PANEL - case 96: { + case AlarmControlPanelCommandRequest::MESSAGE_TYPE: { AlarmControlPanelCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -449,7 +454,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_TEXT - case 99: { + case TextCommandRequest::MESSAGE_TYPE: { TextCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -460,7 +465,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_DATETIME_DATE - case 102: { + case DateCommandRequest::MESSAGE_TYPE: { DateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -471,7 +476,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_DATETIME_TIME - case 105: { + case TimeCommandRequest::MESSAGE_TYPE: { TimeCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -482,7 +487,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 106: { + case VoiceAssistantAudio::MESSAGE_TYPE: { VoiceAssistantAudio msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -493,7 +498,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VALVE - case 111: { + case ValveCommandRequest::MESSAGE_TYPE: { ValveCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -504,7 +509,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_DATETIME_DATETIME - case 114: { + case DateTimeCommandRequest::MESSAGE_TYPE: { DateTimeCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -515,7 +520,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 115: { + case VoiceAssistantTimerEventResponse::MESSAGE_TYPE: { VoiceAssistantTimerEventResponse msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -526,7 +531,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_UPDATE - case 118: { + case UpdateCommandRequest::MESSAGE_TYPE: { UpdateCommandRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -537,7 +542,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 119: { + case VoiceAssistantAnnounceRequest::MESSAGE_TYPE: { VoiceAssistantAnnounceRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -548,9 +553,9 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 121: { + case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: { VoiceAssistantConfigurationRequest msg; - msg.decode(msg_data, msg_size); + // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str()); #endif @@ -559,7 +564,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_VOICE_ASSISTANT - case 123: { + case VoiceAssistantSetConfiguration::MESSAGE_TYPE: { VoiceAssistantSetConfiguration msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -570,7 +575,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_API_NOISE - case 124: { + case NoiseEncryptionSetKeyRequest::MESSAGE_TYPE: { NoiseEncryptionSetKeyRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -581,7 +586,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case 127: { + case BluetoothScannerSetModeRequest::MESSAGE_TYPE: { BluetoothScannerSetModeRequest msg; msg.decode(msg_data, msg_size); #ifdef HAS_PROTO_MESSAGE_DUMP @@ -597,35 +602,28 @@ 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_hello_response(msg)) { 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_connect_response(msg)) { 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_disconnect_response(msg)) { 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_ping_response(msg)) { 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)) { - this->on_fatal_error(); - } + if (this->check_connection_setup_() && !this->send_device_info_response(msg)) { + this->on_fatal_error(); } } void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { @@ -643,23 +641,24 @@ void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest & this->subscribe_logs(msg); } } +#ifdef USE_API_HOMEASSISTANT_SERVICES void APIServerConnection::on_subscribe_homeassistant_services_request( const SubscribeHomeassistantServicesRequest &msg) { if (this->check_authenticated_()) { this->subscribe_homeassistant_services(msg); } } +#endif +#ifdef USE_API_HOMEASSISTANT_STATES void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) { if (this->check_authenticated_()) { this->subscribe_home_assistant_states(msg); } } +#endif 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)) { - this->on_fatal_error(); - } + if (this->check_connection_setup_() && !this->send_get_time_response(msg)) { + this->on_fatal_error(); } } #ifdef USE_API_SERVICES @@ -671,11 +670,8 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest #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)) { - this->on_fatal_error(); - } + if (this->check_authenticated_() && !this->send_noise_encryption_set_key_response(msg)) { + this->on_fatal_error(); } } #endif @@ -865,11 +861,8 @@ void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNo #ifdef USE_BLUETOOTH_PROXY 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)) { - this->on_fatal_error(); - } + if (this->check_authenticated_() && !this->send_subscribe_bluetooth_connections_free_response(msg)) { + this->on_fatal_error(); } } #endif @@ -897,11 +890,8 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo #endif #ifdef USE_VOICE_ASSISTANT 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)) { - this->on_fatal_error(); - } + if (this->check_authenticated_() && !this->send_voice_assistant_get_configuration_response(msg)) { + this->on_fatal_error(); } } #endif @@ -920,5 +910,4 @@ void APIServerConnection::on_alarm_control_panel_command_request(const AlarmCont } #endif -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 9c5dc244fe..6172e33bf6 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -6,8 +6,7 @@ #include "api_pb2.h" -namespace esphome { -namespace api { +namespace esphome::api { class APIServerConnectionBase : public ProtoService { public: @@ -18,11 +17,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){}; @@ -61,11 +60,17 @@ class APIServerConnectionBase : public ProtoService { virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){}; #endif +#ifdef USE_API_HOMEASSISTANT_SERVICES virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; +#endif +#ifdef USE_API_HOMEASSISTANT_STATES virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; +#endif +#ifdef USE_API_HOMEASSISTANT_STATES virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; +#endif virtual void on_get_time_request(const GetTimeRequest &value){}; virtual void on_get_time_response(const GetTimeResponse &value){}; @@ -207,22 +212,26 @@ class APIServerConnectionBase : public ProtoService { class APIServerConnection : public APIServerConnectionBase { public: - virtual HelloResponse hello(const HelloRequest &msg) = 0; - virtual ConnectResponse connect(const ConnectRequest &msg) = 0; - virtual DisconnectResponse disconnect(const DisconnectRequest &msg) = 0; - virtual PingResponse ping(const PingRequest &msg) = 0; - virtual DeviceInfoResponse device_info(const DeviceInfoRequest &msg) = 0; + virtual bool send_hello_response(const HelloRequest &msg) = 0; + virtual bool send_connect_response(const ConnectRequest &msg) = 0; + virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0; + virtual bool send_ping_response(const PingRequest &msg) = 0; + virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0; virtual void list_entities(const ListEntitiesRequest &msg) = 0; virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0; virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; +#ifdef USE_API_HOMEASSISTANT_SERVICES virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; +#endif +#ifdef USE_API_HOMEASSISTANT_STATES virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; - virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; +#endif + virtual bool send_get_time_response(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; + virtual bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) = 0; #endif #ifdef USE_BUTTON virtual void button_command(const ButtonCommandRequest &msg) = 0; @@ -303,7 +312,7 @@ class APIServerConnection : public APIServerConnectionBase { virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0; #endif #ifdef USE_BLUETOOTH_PROXY - virtual BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( + virtual bool send_subscribe_bluetooth_connections_free_response( const SubscribeBluetoothConnectionsFreeRequest &msg) = 0; #endif #ifdef USE_BLUETOOTH_PROXY @@ -316,8 +325,7 @@ class APIServerConnection : public APIServerConnectionBase { virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; #endif #ifdef USE_VOICE_ASSISTANT - virtual VoiceAssistantConfigurationResponse voice_assistant_get_configuration( - const VoiceAssistantConfigurationRequest &msg) = 0; + virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0; #endif #ifdef USE_VOICE_ASSISTANT virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0; @@ -334,8 +342,12 @@ class APIServerConnection : public APIServerConnectionBase { void on_list_entities_request(const ListEntitiesRequest &msg) override; void on_subscribe_states_request(const SubscribeStatesRequest &msg) override; void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override; +#ifdef USE_API_HOMEASSISTANT_SERVICES void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; +#endif +#ifdef USE_API_HOMEASSISTANT_STATES void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; +#endif void on_get_time_request(const GetTimeRequest &msg) override; #ifdef USE_API_SERVICES void on_execute_service_request(const ExecuteServiceRequest &msg) override; @@ -445,5 +457,4 @@ class APIServerConnection : public APIServerConnectionBase { #endif }; -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index f5be672c9a..1f38f4a31a 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -16,8 +16,7 @@ #include -namespace esphome { -namespace api { +namespace esphome::api { static const char *const TAG = "api"; @@ -31,7 +30,6 @@ APIServer::APIServer() { } void APIServer::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->setup_controller(); #ifdef USE_API_NOISE @@ -105,7 +103,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); } }); @@ -185,9 +183,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) { @@ -205,22 +203,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(); @@ -373,12 +369,15 @@ void APIServer::set_password(const std::string &password) { this->password_ = pa void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } +#ifdef USE_API_HOMEASSISTANT_SERVICES void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { for (auto &client : this->clients_) { client->send_homeassistant_service_call(call); } } +#endif +#ifdef USE_API_HOMEASSISTANT_STATES void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, std::function f) { this->state_subs_.push_back(HomeAssistantStateSubscription{ @@ -402,6 +401,7 @@ void APIServer::get_home_assistant_state(std::string entity_id, optional &APIServer::get_state_subs() const { return this->state_subs_; } +#endif uint16_t APIServer::get_port() const { return this->port_; } @@ -428,10 +428,11 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { ESP_LOGD(TAG, "Noise PSK saved"); if (make_active) { this->set_timeout(100, [this, psk]() { - ESP_LOGW(TAG, "Disconnecting all clients to reset connections"); + ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); this->set_noise_psk(psk); for (auto &c : this->clients_) { - c->send_message(DisconnectRequest()); + DisconnectRequest req; + c->send_message(req, DisconnectRequest::MESSAGE_TYPE); } }); } @@ -464,7 +465,8 @@ void APIServer::on_shutdown() { // Send disconnect requests to all connected clients for (auto &c : this->clients_) { - if (!c->send_message(DisconnectRequest())) { + DisconnectRequest req; + if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) { // If we can't send the disconnect request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE, @@ -484,6 +486,5 @@ bool APIServer::teardown() { return this->clients_.empty(); } -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index f41064b62b..8b5e624df2 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -18,8 +18,7 @@ #include -namespace esphome { -namespace api { +namespace esphome::api { #ifdef USE_API_NOISE struct SavedNoisePsk { @@ -39,7 +38,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); @@ -108,7 +106,9 @@ class APIServer : public Component, public Controller { #ifdef USE_MEDIA_PLAYER void on_media_player_update(media_player::MediaPlayer *obj) override; #endif +#ifdef USE_API_HOMEASSISTANT_SERVICES void send_homeassistant_service_call(const HomeassistantServiceResponse &call); +#endif #ifdef USE_API_SERVICES void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #endif @@ -128,6 +128,7 @@ class APIServer : public Component, public Controller { bool is_connected() const; +#ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { std::string entity_id; optional attribute; @@ -140,6 +141,7 @@ 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; +#endif #ifdef USE_API_SERVICES const std::vector &get_user_services() const { return this->user_services_; } #endif @@ -173,7 +175,9 @@ class APIServer : public Component, public Controller { std::string password_; #endif std::vector shared_write_buffer_; // Shared proto write buffer for all connections +#ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; +#endif #ifdef USE_API_SERVICES std::vector user_services_; #endif @@ -197,6 +201,5 @@ template class APIConnectedCondition : public Condition { bool check(Ts... x) override { return global_api_server->is_connected(); } }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 2d4bc37c89..ce018b3b98 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -14,6 +14,8 @@ with warnings.catch_warnings(): from aioesphomeapi import APIClient, parse_log_message from aioesphomeapi.log_runner import async_run +import contextlib + from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.core import CORE @@ -28,7 +30,7 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -async def async_run_logs(config: dict[str, Any], address: str) -> None: +async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: """Run the logs command in the event loop.""" conf = config["api"] name = config["esphome"]["name"] @@ -37,13 +39,21 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: noise_psk: str | None = None if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)): noise_psk = key - _LOGGER.info("Starting log output from %s using esphome API", address) + + if len(addresses) == 1: + _LOGGER.info("Starting log output from %s using esphome API", addresses[0]) + else: + _LOGGER.info( + "Starting log output from %s using esphome API", " or ".join(addresses) + ) + cli = APIClient( - address, + addresses[0], # Primary address for compatibility port, password, client_info=f"ESPHome Logs {__version__}", noise_psk=noise_psk, + addresses=addresses, # Pass all addresses for automatic retry ) dashboard = CORE.dashboard @@ -64,9 +74,7 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None: await stop() -def run_logs(config: dict[str, Any], address: str) -> None: +def run_logs(config: dict[str, Any], addresses: list[str]) -> None: """Run the logs command.""" - try: - asyncio.run(async_run_logs(config, address)) - except KeyboardInterrupt: - pass + with contextlib.suppress(KeyboardInterrupt): + asyncio.run(async_run_logs(config, addresses)) diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 35329c4a5e..44f9eee571 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -6,8 +6,7 @@ #ifdef USE_API_SERVICES #include "user_services.h" #endif -namespace esphome { -namespace api { +namespace esphome::api { #ifdef USE_API_SERVICES template class CustomAPIDeviceService : public UserServiceBase { @@ -57,6 +56,14 @@ class CustomAPIDevice { auto *service = new CustomAPIDeviceService(name, arg_names, (T *) this, callback); // NOLINT global_api_server->register_user_service(service); } +#else + template + void register_service(void (T::*callback)(Ts...), const std::string &name, + const std::array &arg_names) { + static_assert( + sizeof(T) == 0, + "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); + } #endif /** Register a custom native API service that will show up in Home Assistant. @@ -82,8 +89,15 @@ class CustomAPIDevice { auto *service = new CustomAPIDeviceService(name, {}, (T *) this, callback); // NOLINT global_api_server->register_user_service(service); } +#else + template void register_service(void (T::*callback)(), const std::string &name) { + static_assert( + sizeof(T) == 0, + "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); + } #endif +#ifdef USE_API_HOMEASSISTANT_STATES /** Subscribe to the state (or attribute state) of an entity from Home Assistant. * * Usage: @@ -135,7 +149,25 @@ class CustomAPIDevice { auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), f); } +#else + template + void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, + const std::string &attribute = "") { + static_assert(sizeof(T) == 0, + "subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section " + "of your YAML configuration"); + } + template + void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, + const std::string &attribute = "") { + static_assert(sizeof(T) == 0, + "subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section " + "of your YAML configuration"); + } +#endif + +#ifdef USE_API_HOMEASSISTANT_SERVICES /** Call a Home Assistant service from ESPHome. * * Usage: @@ -148,7 +180,7 @@ class CustomAPIDevice { */ void call_homeassistant_service(const std::string &service_name) { HomeassistantServiceResponse resp; - resp.service = service_name; + resp.set_service(StringRef(service_name)); global_api_server->send_homeassistant_service_call(resp); } @@ -168,12 +200,12 @@ class CustomAPIDevice { */ void call_homeassistant_service(const std::string &service_name, const std::map &data) { HomeassistantServiceResponse resp; - resp.service = service_name; + resp.set_service(StringRef(service_name)); for (auto &it : data) { - HomeassistantServiceMap kv; - kv.key = it.first; + resp.data.emplace_back(); + auto &kv = resp.data.back(); + kv.set_key(StringRef(it.first)); kv.value = it.second; - resp.data.push_back(kv); } global_api_server->send_homeassistant_service_call(resp); } @@ -190,7 +222,7 @@ class CustomAPIDevice { */ void fire_homeassistant_event(const std::string &event_name) { HomeassistantServiceResponse resp; - resp.service = event_name; + resp.set_service(StringRef(event_name)); resp.is_event = true; global_api_server->send_homeassistant_service_call(resp); } @@ -210,18 +242,40 @@ class CustomAPIDevice { */ void fire_homeassistant_event(const std::string &service_name, const std::map &data) { HomeassistantServiceResponse resp; - resp.service = service_name; + resp.set_service(StringRef(service_name)); resp.is_event = true; for (auto &it : data) { - HomeassistantServiceMap kv; - kv.key = it.first; + resp.data.emplace_back(); + auto &kv = resp.data.back(); + kv.set_key(StringRef(it.first)); kv.value = it.second; - resp.data.push_back(kv); } global_api_server->send_homeassistant_service_call(resp); } +#else + template void call_homeassistant_service(const std::string &service_name) { + static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' " + "section of your YAML configuration"); + } + + template + void call_homeassistant_service(const std::string &service_name, const std::map &data) { + static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' " + "section of your YAML configuration"); + } + + template void fire_homeassistant_event(const std::string &event_name) { + static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' " + "section of your YAML configuration"); + } + + template + void fire_homeassistant_event(const std::string &service_name, const std::map &data) { + static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' " + "section of your YAML configuration"); + } +#endif }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index f765f1f806..5df9c7c792 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -2,13 +2,13 @@ #include "api_server.h" #ifdef USE_API +#ifdef USE_API_HOMEASSISTANT_SERVICES #include "api_pb2.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" #include -namespace esphome { -namespace api { +namespace esphome::api { template class TemplatableStringValue : public TemplatableValue { private: @@ -36,6 +36,9 @@ template class TemplatableStringValue : public TemplatableValue class TemplatableKeyValuePair { public: + // Keys are always string literals from YAML dictionary keys (e.g., "code", "event") + // and never templatable values or lambdas. Only the value parameter can be a lambda/template. + // Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues. template TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} std::string key; TemplatableStringValue value; @@ -47,37 +50,39 @@ template class HomeAssistantServiceCallAction : public Action void set_service(T service) { this->service_ = service; } - template void add_data(std::string key, T value) { - this->data_.push_back(TemplatableKeyValuePair(key, value)); - } + // Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))). + // The value parameter can be a lambda/template, but keys are never templatable. + // Using pass-by-value allows the compiler to optimize for both lvalues and rvalues. + template void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); } template void add_data_template(std::string key, T value) { - this->data_template_.push_back(TemplatableKeyValuePair(key, value)); + this->data_template_.emplace_back(std::move(key), value); } template void add_variable(std::string key, T value) { - this->variables_.push_back(TemplatableKeyValuePair(key, value)); + this->variables_.emplace_back(std::move(key), value); } void play(Ts... x) override { HomeassistantServiceResponse resp; - resp.service = this->service_.value(x...); + std::string service_value = this->service_.value(x...); + resp.set_service(StringRef(service_value)); resp.is_event = this->is_event_; for (auto &it : this->data_) { - HomeassistantServiceMap kv; - kv.key = it.key; + resp.data.emplace_back(); + auto &kv = resp.data.back(); + kv.set_key(StringRef(it.key)); kv.value = it.value.value(x...); - resp.data.push_back(kv); } for (auto &it : this->data_template_) { - HomeassistantServiceMap kv; - kv.key = it.key; + resp.data_template.emplace_back(); + auto &kv = resp.data_template.back(); + kv.set_key(StringRef(it.key)); kv.value = it.value.value(x...); - resp.data_template.push_back(kv); } for (auto &it : this->variables_) { - HomeassistantServiceMap kv; - kv.key = it.key; + resp.variables.emplace_back(); + auto &kv = resp.variables.back(); + kv.set_key(StringRef(it.key)); kv.value = it.value.value(x...); - resp.variables.push_back(kv); } this->parent_->send_homeassistant_service_call(resp); } @@ -91,6 +96,6 @@ template class HomeAssistantServiceCallAction : public Action> variables_; }; -} // namespace api -} // namespace esphome +} // namespace esphome::api +#endif #endif diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 1fbe68117b..da4800a45e 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -6,8 +6,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace api { +namespace esphome::api { // Generate entity handler implementations using macros #ifdef USE_BINARY_SENSOR @@ -86,10 +85,9 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie #ifdef USE_API_SERVICES bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); - return this->client_->send_message(resp); + return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); } #endif -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index b4cbf6c489..769d7b9b6e 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -4,8 +4,7 @@ #ifdef USE_API #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" -namespace esphome { -namespace api { +namespace esphome::api { class APIConnection; @@ -96,6 +95,5 @@ class ListEntitiesIterator : public ComponentIterator { APIConnection *client_; }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 25daf17ccc..cb6c07ec3c 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -3,12 +3,11 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace api { +namespace esphome::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) { @@ -89,5 +88,4 @@ std::string ProtoMessage::dump() const { } #endif -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index a435168821..0e5ec61050 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -3,16 +3,64 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" #include +#include #include #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE #define HAS_PROTO_MESSAGE_DUMP #endif -namespace esphome { -namespace api { +namespace esphome::api { + +// Helper functions for ZigZag encoding/decoding +inline constexpr uint32_t encode_zigzag32(int32_t value) { + return (static_cast(value) << 1) ^ (static_cast(value >> 31)); +} + +inline constexpr uint64_t encode_zigzag64(int64_t value) { + return (static_cast(value) << 1) ^ (static_cast(value >> 63)); +} + +inline constexpr int32_t decode_zigzag32(uint32_t value) { + return (value & 1) ? static_cast(~(value >> 1)) : static_cast(value >> 1); +} + +inline constexpr int64_t decode_zigzag64(uint64_t value) { + return (value & 1) ? static_cast(~(value >> 1)) : static_cast(value >> 1); +} + +/* + * StringRef Ownership Model for API Protocol Messages + * =================================================== + * + * StringRef is used for zero-copy string handling in outgoing (SOURCE_SERVER) messages. + * It holds a pointer and length to existing string data without copying. + * + * CRITICAL: The referenced string data MUST remain valid until message encoding completes. + * + * Safe StringRef Patterns: + * 1. String literals: StringRef("literal") - Always safe (static storage duration) + * 2. Member variables: StringRef(this->member_string_) - Safe if object outlives encoding + * 3. Global/static strings: StringRef(GLOBAL_CONSTANT) - Always safe + * 4. Local variables: Safe ONLY if encoding happens before function returns: + * std::string temp = compute_value(); + * msg.set_field(StringRef(temp)); + * return this->send_message(msg); // temp is valid during encoding + * + * Unsafe Patterns (WILL cause crashes/corruption): + * 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value + * 2. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary + * + * For unsafe patterns, store in a local variable first: + * std::string temp = get_string(); // or str1 + str2 + * msg.set_field(StringRef(temp)); + * + * The send_*_response pattern ensures proper lifetime management by encoding + * within the same function scope where temporaries are created. + */ /// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit class ProtoVarInt { @@ -56,33 +104,25 @@ class ProtoVarInt { return {}; // Incomplete or invalid varint } - uint16_t as_uint16() const { return this->value_; } - uint32_t as_uint32() const { return this->value_; } - uint64_t as_uint64() const { return this->value_; } - bool as_bool() const { return this->value_; } - int32_t as_int32() const { + constexpr uint16_t as_uint16() const { return this->value_; } + constexpr uint32_t as_uint32() const { return this->value_; } + constexpr uint64_t as_uint64() const { return this->value_; } + constexpr bool as_bool() const { return this->value_; } + constexpr int32_t as_int32() const { // Not ZigZag encoded return static_cast(this->as_int64()); } - int64_t as_int64() const { + constexpr int64_t as_int64() const { // Not ZigZag encoded return static_cast(this->value_); } - int32_t as_sint32() const { + constexpr int32_t as_sint32() const { // with ZigZag encoding - if (this->value_ & 1) { - return static_cast(~(this->value_ >> 1)); - } else { - return static_cast(this->value_ >> 1); - } + return decode_zigzag32(static_cast(this->value_)); } - int64_t as_sint64() const { + constexpr int64_t as_sint64() const { // with ZigZag encoding - if (this->value_ & 1) { - return static_cast(~(this->value_ >> 1)); - } else { - return static_cast(this->value_ >> 1); - } + return decode_zigzag64(this->value_); } /** * Encode the varint value to a pre-allocated buffer without bounds checking. @@ -135,6 +175,7 @@ class ProtoVarInt { // Forward declaration for decode_to_message and encode_to_writer class ProtoMessage; +class ProtoDecodableMessage; class ProtoLengthDelimited { public: @@ -142,15 +183,15 @@ class ProtoLengthDelimited { std::string as_string() const { return std::string(reinterpret_cast(this->value_), this->length_); } /** - * Decode the length-delimited data into an existing ProtoMessage instance. + * Decode the length-delimited data into an existing ProtoDecodableMessage instance. * * This method allows decoding without templates, enabling use in contexts - * where the message type is not known at compile time. The ProtoMessage's + * where the message type is not known at compile time. The ProtoDecodableMessage's * decode() method will be called with the raw data and length. * - * @param msg The ProtoMessage instance to decode into + * @param msg The ProtoDecodableMessage instance to decode into */ - void decode_to_message(ProtoMessage &msg) const; + void decode_to_message(ProtoDecodableMessage &msg) const; protected: const uint8_t *const value_; @@ -175,23 +216,7 @@ class Proto32Bit { const uint32_t value_; }; -class Proto64Bit { - public: - explicit Proto64Bit(uint64_t value) : value_(value) {} - uint64_t as_fixed64() const { return this->value_; } - int64_t as_sfixed64() const { return static_cast(this->value_); } - double as_double() const { - union { - uint64_t raw; - double value; - } s{}; - s.raw = this->value_; - return s.value; - } - - protected: - const uint64_t value_; -}; +// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported class ProtoWriteBuffer { public: @@ -205,9 +230,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 */ @@ -221,12 +246,20 @@ class ProtoWriteBuffer { this->encode_field_raw(field_id, 2); // type 2: Length-delimited string this->encode_varint_raw(len); - auto *data = reinterpret_cast(string); - this->buffer_->insert(this->buffer_->end(), data, data + len); + + // Using resize + memcpy instead of insert provides significant performance improvement: + // ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings + // as it avoids iterator checks and potential element moves that insert performs + size_t old_size = this->buffer_->size(); + this->buffer_->resize(old_size + len); + std::memcpy(this->buffer_->data() + old_size, string, len); } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { this->encode_string(field_id, value.data(), value.size(), force); } + void encode_string(uint32_t field_id, const StringRef &ref, bool force = false) { + this->encode_string(field_id, ref.c_str(), ref.size(), force); + } void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { this->encode_string(field_id, reinterpret_cast(data), len, force); } @@ -258,20 +291,10 @@ class ProtoWriteBuffer { this->write((value >> 16) & 0xFF); this->write((value >> 24) & 0xFF); } - void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) { - if (value == 0 && !force) - return; - - this->encode_field_raw(field_id, 1); // type 1: 64-bit fixed64 - this->write((value >> 0) & 0xFF); - this->write((value >> 8) & 0xFF); - this->write((value >> 16) & 0xFF); - this->write((value >> 24) & 0xFF); - this->write((value >> 32) & 0xFF); - this->write((value >> 40) & 0xFF); - this->write((value >> 48) & 0xFF); - this->write((value >> 56) & 0xFF); - } + // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally + // not supported to reduce overhead on embedded systems. All ESPHome devices are + // 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support + // is needed in the future, the necessary encoding/decoding functions must be added. void encode_float(uint32_t field_id, float value, bool force = false) { if (value == 0.0f && !force) return; @@ -295,22 +318,10 @@ class ProtoWriteBuffer { this->encode_uint64(field_id, static_cast(value), force); } void encode_sint32(uint32_t field_id, int32_t value, bool force = false) { - uint32_t uvalue; - if (value < 0) { - uvalue = ~(value << 1); - } else { - uvalue = value << 1; - } - this->encode_uint32(field_id, uvalue, force); + this->encode_uint32(field_id, encode_zigzag32(value), force); } void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { - uint64_t uvalue; - if (value < 0) { - uvalue = ~(value << 1); - } else { - uvalue = value << 1; - } - this->encode_uint64(field_id, uvalue, force); + this->encode_uint64(field_id, encode_zigzag64(value), force); } void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false); std::vector *get_buffer() const { return buffer_; } @@ -319,53 +330,69 @@ class ProtoWriteBuffer { std::vector *buffer_; }; +// Forward declaration +class ProtoSize; + class ProtoMessage { public: 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 {} + virtual void calculate_size(ProtoSize &size) const {} #ifdef HAS_PROTO_MESSAGE_DUMP std::string dump() const; 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 { + private: + uint32_t total_size_ = 0; + 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. + * This class provides methods to calculate the exact byte counts needed + * for encoding various Protocol Buffer field types. The class now uses an + * object-based approach to reduce parameter passing overhead while keeping + * varint calculation methods static for external use. * * Implements Protocol Buffer encoding size calculation according to: * https://protobuf.dev/programming-guides/encoding/ * * Key features: + * - Object-based approach reduces flash usage by eliminating parameter passing * - Early-return optimization for zero/default values - * - Direct total_size updates to avoid unnecessary additions + * - Static varint methods for external callers * - Specialized handling for different field types according to protobuf spec - * - Templated helpers for repeated fields and messages */ + ProtoSize() = default; + + uint32_t get_size() const { return total_size_; } + /** * @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) { + static constexpr uint32_t varint(uint32_t value) { // Optimized varint size calculation using leading zeros // Each 7 bits requires one byte in the varint encoding if (value < 128) @@ -389,7 +416,7 @@ class ProtoSize { * @param value The uint64_t value to calculate size for * @return The number of bytes needed to encode the value */ - static inline uint32_t varint(uint64_t value) { + static constexpr uint32_t varint(uint64_t value) { // Handle common case of values fitting in uint32_t (vast majority of use cases) if (value <= UINT32_MAX) { return varint(static_cast(value)); @@ -420,7 +447,7 @@ class ProtoSize { * @param value The int32_t value to calculate size for * @return The number of bytes needed to encode the value */ - static inline uint32_t varint(int32_t value) { + static constexpr uint32_t varint(int32_t value) { // Negative values are sign-extended to 64 bits in protocol buffers, // which always results in a 10-byte varint for negative int32 if (value < 0) { @@ -436,7 +463,7 @@ class ProtoSize { * @param value The int64_t value to calculate size for * @return The number of bytes needed to encode the value */ - static inline uint32_t varint(int64_t value) { + static constexpr uint32_t varint(int64_t value) { // For int64_t, we convert to uint64_t and calculate the size // This works because the bit pattern determines the encoding size, // and we've handled negative int32 values as a special case above @@ -450,7 +477,7 @@ class ProtoSize { * @param type The wire type value (from the WireType enum in the protobuf spec) * @return The number of bytes needed to encode the field ID and wire type */ - static inline uint32_t field(uint32_t field_id, uint32_t type) { + static constexpr uint32_t field(uint32_t field_id, uint32_t type) { uint32_t tag = (field_id << 3) | (type & 0b111); return varint(tag); } @@ -459,9 +486,7 @@ class ProtoSize { * @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 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 * @@ -474,244 +499,180 @@ class ProtoSize { /** * @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)); + inline void add_int32(uint32_t field_id_size, int32_t value) { + if (value != 0) { + add_int32_force(field_id_size, value); } } /** - * @brief Calculates and adds the size of an int32 field to the total message size (repeated field version) + * @brief Calculates and adds the size of an int32 field to the total message size (force 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)); - } + inline void add_int32_force(uint32_t field_id_size, int32_t value) { + // Always calculate size when forced + // Negative values are encoded as 10-byte varints in protobuf + total_size_ += field_id_size + (value < 0 ? 10 : 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 + inline void add_uint32(uint32_t field_id_size, uint32_t value) { + if (value != 0) { + add_uint32_force(field_id_size, value); } - - // 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) + * @brief Calculates and adds the size of a uint32 field to the total message size (force 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); + inline void add_uint32_force(uint32_t field_id_size, uint32_t value) { + // Always calculate size when force is true + 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 + inline void add_bool(uint32_t field_id_size, bool value) { + if (value) { + // Boolean fields always use 1 byte when true + total_size_ += field_id_size + 1; } - - // 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) + * @brief Calculates and adds the size of a boolean field to the total message size (force 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 + inline void add_bool_force(uint32_t field_id_size, bool value) { + // Always calculate size when force is true // Boolean fields always use 1 byte - total_size += field_id_size + 1; + 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 + * @brief Calculates and adds the size of a float field to the total message size */ - 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 + inline void add_float(uint32_t field_id_size, float value) { + if (value != 0.0f) { + total_size_ += field_id_size + 4; } - - // Fixed fields always take exactly NumBytes - total_size += field_id_size + NumBytes; } + // 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 an enum field to the total message size - * - * Enum fields are encoded as uint32 varints. + * @brief Calculates and adds the size of a fixed32 field to the total message size */ - 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 + inline void add_fixed32(uint32_t field_id_size, uint32_t value) { + if (value != 0) { + total_size_ += field_id_size + 4; } - - // Enums are encoded as uint32 - total_size += field_id_size + varint(value); } + // 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 an enum field to the total message size (repeated field version) - * - * Enum fields are encoded as uint32 varints. + * @brief Calculates and adds the size of a sfixed32 field to the total message size */ - 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); + inline void add_sfixed32(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 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 + inline void add_sint32(uint32_t field_id_size, int32_t value) { + if (value != 0) { + add_sint32_force(field_id_size, value); } - - // 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) + * @brief Calculates and adds the size of a sint32 field to the total message size (force 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); + inline void add_sint32_force(uint32_t field_id_size, int32_t value) { + // Always calculate size when force is true + // ZigZag encoding for sint32 + total_size_ += field_id_size + varint(encode_zigzag32(value)); } /** * @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 + inline void add_int64(uint32_t field_id_size, int64_t value) { + if (value != 0) { + add_int64_force(field_id_size, value); } - - // 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) + * @brief Calculates and adds the size of an int64 field to the total message size (force 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); + inline void add_int64_force(uint32_t field_id_size, int64_t value) { + // Always calculate size when force is true + 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 + inline void add_uint64(uint32_t field_id_size, uint64_t value) { + if (value != 0) { + add_uint64_force(field_id_size, value); } - - // 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) + * @brief Calculates and adds the size of a uint64 field to the total message size (force 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); + inline void add_uint64_force(uint32_t field_id_size, uint64_t value) { + // Always calculate size when force is true + total_size_ += field_id_size + varint(value); + } + + // NOTE: sint64 support functions (add_sint64_field, add_sint64_field_force) removed + // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems + + /** + * @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size + */ + inline void add_length(uint32_t field_id_size, size_t len) { + if (len != 0) { + add_length_force(field_id_size, len); + } } /** - * @brief Calculates and adds the size of a sint64 field to the total message size + * @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size (repeated + * field version) + */ + inline void add_length_force(uint32_t field_id_size, size_t len) { + // Always calculate size when force is true + // Field ID + length varint + data bytes + total_size_ += field_id_size + varint(static_cast(len)) + static_cast(len); + } + + /** + * @brief Adds a pre-calculated size directly to the total * - * Sint64 fields use ZigZag encoding, which is more efficient for negative values. - */ - static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) { - // Skip calculation if value is zero - if (value == 0) { - return; // No need to update total_size - } - - // ZigZag encoding for sint64: (n << 1) ^ (n >> 63) - uint64_t zigzag = (static_cast(value) << 1) ^ (static_cast(value >> 63)); - total_size += field_id_size + varint(zigzag); - } - - /** - * @brief Calculates and adds the size of a sint64 field to the total message size (repeated field version) + * This is used when we can calculate the total size by multiplying the number + * of elements by the bytes per element (for repeated fixed-size types like float, fixed32, etc.) * - * Sint64 fields use ZigZag encoding, which is more efficient for negative values. + * @param size The pre-calculated total size to add */ - static inline void add_sint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) { - // Always calculate size for repeated fields - // ZigZag encoding for sint64: (n << 1) ^ (n >> 63) - uint64_t zigzag = (static_cast(value) << 1) ^ (static_cast(value >> 63)); - total_size += field_id_size + varint(zigzag); - } - - /** - * @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; - } + inline void add_precalculated_size(uint32_t size) { total_size_ += size; } /** * @brief Calculates and adds the size of a nested message field to the total message size @@ -721,26 +682,21 @@ class ProtoSize { * * @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 + inline void add_message_field(uint32_t field_id_size, uint32_t nested_size) { + if (nested_size != 0) { + add_message_field_force(field_id_size, nested_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) + * @brief Calculates and adds the size of a nested message field to the total message size (force 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 + inline void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) { + // Always calculate size when force is true // Field ID + length varint + nested message content - total_size += field_id_size + varint(nested_size) + nested_size; + total_size_ += field_id_size + varint(nested_size) + nested_size; } /** @@ -752,26 +708,29 @@ class ProtoSize { * * @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); + inline void add_message_object(uint32_t field_id_size, const ProtoMessage &message) { + // Calculate nested message size by creating a temporary ProtoSize + ProtoSize nested_calc; + message.calculate_size(nested_calc); + uint32_t nested_size = nested_calc.get_size(); // Use the base implementation with the calculated nested_size - add_message_field(total_size, field_id_size, nested_size); + add_message_field(field_id_size, nested_size); } /** - * @brief Calculates and adds the size of a nested message field to the total message size (repeated field version) + * @brief Calculates and adds the size of a nested message field to the total message size (force 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); + inline void add_message_object_force(uint32_t field_id_size, const ProtoMessage &message) { + // Calculate nested message size by creating a temporary ProtoSize + ProtoSize nested_calc; + message.calculate_size(nested_calc); + uint32_t nested_size = nested_calc.get_size(); // Use the base implementation with the calculated nested_size - add_message_field_repeated(total_size, field_id_size, nested_size); + add_message_field_force(field_id_size, nested_size); } /** @@ -784,16 +743,15 @@ class ProtoSize { * @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) { + inline void add_repeated_message(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 + // Use the force version for all messages in the repeated field for (const auto &message : messages) { - add_message_object_repeated(total_size, field_id_size, message); + add_message_object_force(field_id_size, message); } } }; @@ -803,8 +761,9 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa 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); + ProtoSize msg_size; + value.calculate_size(msg_size); + uint32_t msg_length_bytes = msg_size.get_size(); // Calculate how many bytes the length varint needs uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes); @@ -823,8 +782,8 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); } -// Implementation of decode_to_message - must be after ProtoMessage is defined -inline void ProtoLengthDelimited::decode_to_message(ProtoMessage &msg) const { +// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined +inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) const { msg.decode(this->value_, this->length_); } @@ -836,7 +795,9 @@ class ProtoService { virtual bool is_authenticated() = 0; virtual bool is_connection_setup() = 0; virtual void on_fatal_error() = 0; +#ifdef USE_API_PASSWORD virtual void on_unauthenticated_access() = 0; +#endif virtual void on_no_setup_connection() = 0; /** * Create a buffer with a reserved size. @@ -851,8 +812,9 @@ class ProtoService { // Optimized method that pre-allocates buffer based on message size bool send_message_(const ProtoMessage &msg, uint8_t message_type) { - uint32_t msg_size = 0; - msg.calculate_size(msg_size); + ProtoSize size; + msg.calculate_size(size); + uint32_t msg_size = size.get_size(); // Create a pre-sized buffer auto buffer = this->create_buffer(msg_size); @@ -874,6 +836,7 @@ class ProtoService { } bool check_authenticated_() { +#ifdef USE_API_PASSWORD if (!this->check_connection_setup_()) { return false; } @@ -882,8 +845,10 @@ class ProtoService { return false; } return true; +#else + return this->check_connection_setup_(); +#endif } }; -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 12accf4613..3a563f2221 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -3,8 +3,7 @@ #include "api_connection.h" #include "esphome/core/log.h" -namespace esphome { -namespace api { +namespace esphome::api { // Generate entity handler implementations using macros #ifdef USE_BINARY_SENSOR @@ -69,6 +68,5 @@ INITIAL_STATE_HANDLER(update, update::UpdateEntity) InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 2b7b508056..2c22c322ec 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" #include "esphome/core/controller.h" -namespace esphome { -namespace api { +namespace esphome::api { class APIConnection; @@ -89,6 +88,5 @@ class InitialStateIterator : public ComponentIterator { APIConnection *client_; }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 7e73722a92..27b30eb332 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -1,8 +1,7 @@ #include "user_services.h" #include "esphome/core/log.h" -namespace esphome { -namespace api { +namespace esphome::api { template<> bool get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.bool_; } template<> int32_t get_execute_arg_value(const ExecuteServiceArgument &arg) { @@ -40,5 +39,4 @@ template<> enums::ServiceArgType to_service_arg_type>() return enums::SERVICE_ARG_TYPE_STRING_ARRAY; } -} // namespace api -} // namespace esphome +} // namespace esphome::api diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 93cea8133f..5f040e8433 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -8,14 +8,15 @@ #include "api_pb2.h" #ifdef USE_API_SERVICES -namespace esphome { -namespace api { +namespace esphome::api { class UserServiceDescriptor { public: 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); @@ -31,14 +32,14 @@ template class UserServiceBase : public UserServiceDescriptor { ListEntitiesServicesResponse encode_list_service_response() override { ListEntitiesServicesResponse msg; - msg.name = this->name_; + msg.set_name(StringRef(this->name_)); msg.key = this->key_; std::array arg_types = {to_service_arg_type()...}; for (int i = 0; i < sizeof...(Ts); i++) { - ListEntitiesServicesArgument arg; + msg.args.emplace_back(); + auto &arg = msg.args.back(); arg.type = arg_types[i]; - arg.name = this->arg_names_[i]; - msg.args.push_back(arg); + arg.set_name(StringRef(this->arg_names_[i])); } return msg; } @@ -72,6 +73,5 @@ template class UserServiceTrigger : public UserServiceBasetrigger(x...); } // NOLINT }; -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif // USE_API_SERVICES diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index 5e6d62b284..2609af07d3 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -7,8 +7,6 @@ namespace as3935 { static const char *const TAG = "as3935"; void AS3935Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->irq_pin_->setup(); LOG_PIN(" IRQ Pin: ", this->irq_pin_); diff --git a/esphome/components/as3935_spi/as3935_spi.cpp b/esphome/components/as3935_spi/as3935_spi.cpp index 3a517df56d..1b2e9ccd3f 100644 --- a/esphome/components/as3935_spi/as3935_spi.cpp +++ b/esphome/components/as3935_spi/as3935_spi.cpp @@ -7,9 +7,7 @@ namespace as3935_spi { static const char *const TAG = "as3935_spi"; void SPIAS3935Component::setup() { - ESP_LOGI(TAG, "SPIAS3935Component setup started!"); this->spi_setup(); - ESP_LOGI(TAG, "SPI setup finished!"); AS3935Component::setup(); } diff --git a/esphome/components/as5600/__init__.py b/esphome/components/as5600/__init__.py index 1a437a68a2..acb1c4d9db 100644 --- a/esphome/components/as5600/__init__.py +++ b/esphome/components/as5600/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_DIRECTION, CONF_HYSTERESIS, CONF_ID, + CONF_POWER_MODE, CONF_RANGE, ) @@ -57,7 +58,6 @@ FAST_FILTER = { CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" CONF_WATCHDOG = "watchdog" -CONF_POWER_MODE = "power_mode" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" CONF_START_POSITION = "start_position" diff --git a/esphome/components/as5600/as5600.cpp b/esphome/components/as5600/as5600.cpp index ff29ae5cd4..ee3083d561 100644 --- a/esphome/components/as5600/as5600.cpp +++ b/esphome/components/as5600/as5600.cpp @@ -23,8 +23,6 @@ static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R void AS5600Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - if (!this->read_byte(REGISTER_STATUS).has_value()) { this->mark_failed(); return; diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py index cfc38d796d..1491852e07 100644 --- a/esphome/components/as5600/sensor/__init__.py +++ b/esphome/components/as5600/sensor/__init__.py @@ -24,7 +24,6 @@ AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingCompone CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" CONF_WATCHDOG = "watchdog" -CONF_POWER_MODE = "power_mode" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" CONF_PWM_FREQUENCY = "pwm_frequency" diff --git a/esphome/components/as7341/as7341.cpp b/esphome/components/as7341/as7341.cpp index 1e335f43ad..893eaa850f 100644 --- a/esphome/components/as7341/as7341.cpp +++ b/esphome/components/as7341/as7341.cpp @@ -8,7 +8,6 @@ namespace as7341 { static const char *const TAG = "as7341"; void AS7341Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); LOG_I2C_DEVICE(this); // Verify device ID diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 4a469fa0e0..942d5bc8e5 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -10,7 +10,7 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] CONFIG_SCHEMA = cv.All( cv.Schema({}), diff --git a/esphome/components/at581x/at581x.cpp b/esphome/components/at581x/at581x.cpp index b4f817b737..6804a7f4b5 100644 --- a/esphome/components/at581x/at581x.cpp +++ b/esphome/components/at581x/at581x.cpp @@ -71,7 +71,7 @@ bool AT581XComponent::i2c_read_reg(uint8_t addr, uint8_t &data) { return this->read_register(addr, &data, 1) == esphome::i2c::NO_ERROR; } -void AT581XComponent::setup() { ESP_LOGCONFIG(TAG, "Running setup"); } +void AT581XComponent::setup() {} void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); } #define ARRAY_SIZE(X) (sizeof(X) / sizeof((X)[0])) bool AT581XComponent::i2c_write_config() { diff --git a/esphome/components/atm90e26/atm90e26.cpp b/esphome/components/atm90e26/atm90e26.cpp index ce254f9532..cadc06ac6b 100644 --- a/esphome/components/atm90e26/atm90e26.cpp +++ b/esphome/components/atm90e26/atm90e26.cpp @@ -41,7 +41,6 @@ void ATM90E26Component::update() { } void ATM90E26Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); uint16_t mmode = 0x422; // default values for everything but L/N line current gains diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 4669a59e39..d5fee6ea04 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -109,8 +109,9 @@ void ATM90E32Component::update() { } void ATM90E32Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); + this->cs_summary_ = this->cs_->dump_summary(); + const char *cs = this->cs_summary_.c_str(); uint16_t mmode0 = 0x87; // 3P4W 50Hz uint16_t high_thresh = 0; @@ -131,9 +132,9 @@ void ATM90E32Component::setup() { mmode0 |= 0 << 1; // sets 1st bit to 0, phase b is not counted into the all-phase sum energy/power (P/Q/S) } - this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset - delay(6); // Wait for the minimum 5ms + 1ms - this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access + this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A, false); // Perform soft reset + delay(6); // Wait for the minimum 5ms + 1ms + this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access if (!this->validate_spi_read_(0x55AA, "setup()")) { ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings"); this->mark_failed(); @@ -157,16 +158,17 @@ void ATM90E32Component::setup() { if (this->enable_offset_calibration_) { // Initialize flash storage for offset calibrations - uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_->dump_summary()); + uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_summary_); this->offset_pref_ = global_preferences->make_preference(o_hash, true); this->restore_offset_calibrations_(); // Initialize flash storage for power offset calibrations - uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_->dump_summary()); + uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_summary_); this->power_offset_pref_ = global_preferences->make_preference(po_hash, true); this->restore_power_offset_calibrations_(); } else { - ESP_LOGI(TAG, "[CALIBRATION] Power & Voltage/Current offset calibration is disabled. Using config file values."); + ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.", + cs); for (uint8_t phase = 0; phase < 3; ++phase) { this->write16_(this->voltage_offset_registers[phase], static_cast(this->offset_phase_[phase].voltage_offset_)); @@ -181,21 +183,18 @@ void ATM90E32Component::setup() { if (this->enable_gain_calibration_) { // Initialize flash storage for gain calibration - uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_->dump_summary()); + uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_summary_); this->gain_calibration_pref_ = global_preferences->make_preference(g_hash, true); this->restore_gain_calibrations_(); - if (this->using_saved_calibrations_) { - ESP_LOGI(TAG, "[CALIBRATION] Successfully restored gain calibration from memory."); - } else { + if (!this->using_saved_calibrations_) { for (uint8_t phase = 0; phase < 3; ++phase) { this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_); this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_); } } } else { - ESP_LOGI(TAG, "[CALIBRATION] Gain calibration is disabled. Using config file values."); - + ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs); for (uint8_t phase = 0; phase < 3; ++phase) { this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_); this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_); @@ -214,6 +213,122 @@ void ATM90E32Component::setup() { this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration } +void ATM90E32Component::log_calibration_status_() { + const char *cs = this->cs_summary_.c_str(); + + bool offset_mismatch = false; + bool power_mismatch = false; + bool gain_mismatch = false; + + for (uint8_t phase = 0; phase < 3; ++phase) { + offset_mismatch |= this->offset_calibration_mismatch_[phase]; + power_mismatch |= this->power_offset_calibration_mismatch_[phase]; + gain_mismatch |= this->gain_calibration_mismatch_[phase]; + } + + if (offset_mismatch) { + ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGW(TAG, + "[CALIBRATION][%s] ===================== Offset mismatch: using flash values =====================", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + for (uint8_t phase = 0; phase < 3; ++phase) { + ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase, + this->config_offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].voltage_offset_, + this->config_offset_phase_[phase].current_offset_, this->offset_phase_[phase].current_offset_); + } + ESP_LOGW(TAG, + "[CALIBRATION][%s] ===============================================================================", cs); + } + if (power_mismatch) { + ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGW(TAG, + "[CALIBRATION][%s] ================= Power offset mismatch: using flash values =================", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | offset_active_power|offset_reactive_power|", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + for (uint8_t phase = 0; phase < 3; ++phase) { + ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase, + this->config_power_offset_phase_[phase].active_power_offset, + this->power_offset_phase_[phase].active_power_offset, + this->config_power_offset_phase_[phase].reactive_power_offset, + this->power_offset_phase_[phase].reactive_power_offset); + } + ESP_LOGW(TAG, + "[CALIBRATION][%s] ===============================================================================", cs); + } + if (gain_mismatch) { + ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGW(TAG, + "[CALIBRATION][%s] ====================== Gain mismatch: using flash values =====================", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs); + ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------", + cs); + for (uint8_t phase = 0; phase < 3; ++phase) { + ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6u | %6u | %6u | %6u |", cs, 'A' + phase, + this->config_gain_phase_[phase].voltage_gain, this->gain_phase_[phase].voltage_gain, + this->config_gain_phase_[phase].current_gain, this->gain_phase_[phase].current_gain); + } + ESP_LOGW(TAG, + "[CALIBRATION][%s] ===============================================================================", cs); + } + if (!this->enable_offset_calibration_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.", + cs); + } else if (this->restored_offset_calibration_ && !offset_mismatch) { + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ============== Restored offset calibration from memory ==============", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, + this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\\n", cs); + } + + if (this->restored_power_offset_calibration_ && !power_mismatch) { + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ============ Restored power offset calibration from memory ============", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, + this->power_offset_phase_[phase].active_power_offset, + this->power_offset_phase_[phase].reactive_power_offset); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); + } + if (!this->enable_gain_calibration_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs); + } else if (this->restored_gain_calibration_ && !gain_mismatch) { + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ============ Restoring saved gain calibrations to registers ============", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase, + this->gain_phase_[phase].voltage_gain, this->gain_phase_[phase].current_gain); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\\n", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration loaded and verified successfully.\n", cs); + } + this->calibration_message_printed_ = true; +} + void ATM90E32Component::dump_config() { ESP_LOGCONFIG("", "ATM90E32:"); LOG_PIN(" CS Pin: ", this->cs_); @@ -256,6 +371,10 @@ void ATM90E32Component::dump_config() { LOG_SENSOR(" ", "Peak Current C", this->phase_[PHASEC].peak_current_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_); LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); + if (this->restored_offset_calibration_ || this->restored_power_offset_calibration_ || + this->restored_gain_calibration_ || !this->enable_offset_calibration_ || !this->enable_gain_calibration_) { + this->log_calibration_status_(); + } } float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; } @@ -263,26 +382,35 @@ float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; // R/C registers can conly be cleared after the LastSPIData register is updated (register 78H) // Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period // Default is 143FH (20ms, 63ms) -uint16_t ATM90E32Component::read16_(uint16_t a_register) { +uint16_t ATM90E32Component::read16_transaction_(uint16_t a_register) { uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03); uint8_t addrl = (a_register & 0xFF); - uint8_t data[2]; - uint16_t output; - this->enable(); - delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1ms is plenty - this->write_byte(addrh); - this->write_byte(addrl); - this->read_array(data, 2); - this->disable(); - - output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); + uint8_t data[4] = {addrh, addrl, 0x00, 0x00}; + this->transfer_array(data, 4); + uint16_t output = encode_uint16(data[2], data[3]); ESP_LOGVV(TAG, "read16_ 0x%04" PRIX16 " output 0x%04" PRIX16, a_register, output); return output; } +uint16_t ATM90E32Component::read16_(uint16_t a_register) { + this->enable(); + delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1us is plenty + uint16_t output = this->read16_transaction_(a_register); + delay_microseconds_safe(1); // allow the last clock to propagate before releasing CS + this->disable(); + delay_microseconds_safe(1); // meet minimum CS high time before next transaction + return output; +} + int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) { - const uint16_t val_h = this->read16_(addr_h); - const uint16_t val_l = this->read16_(addr_l); + this->enable(); + delay_microseconds_safe(1); + const uint16_t val_h = this->read16_transaction_(addr_h); + delay_microseconds_safe(1); + const uint16_t val_l = this->read16_transaction_(addr_l); + delay_microseconds_safe(1); + this->disable(); + delay_microseconds_safe(1); const int32_t val = (val_h << 16) | val_l; ESP_LOGVV(TAG, @@ -293,13 +421,19 @@ int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) { return val; } -void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) { +void ATM90E32Component::write16_(uint16_t a_register, uint16_t val, bool validate) { ESP_LOGVV(TAG, "write16_ 0x%04" PRIX16 " val 0x%04" PRIX16, a_register, val); + uint8_t addrh = ((a_register >> 8) & 0x03); + uint8_t addrl = (a_register & 0xFF); + uint8_t data[4] = {addrh, addrl, uint8_t((val >> 8) & 0xFF), uint8_t(val & 0xFF)}; this->enable(); - this->write_byte16(a_register); - this->write_byte16(val); + delay_microseconds_safe(1); // ensure CS setup time + this->write_array(data, 4); + delay_microseconds_safe(1); // allow clock to settle before raising CS this->disable(); - this->validate_spi_read_(val, "write16()"); + delay_microseconds_safe(1); // ensure minimum CS high time + if (validate) + this->validate_spi_read_(val, "write16()"); } float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; } @@ -442,8 +576,10 @@ float ATM90E32Component::get_chip_temperature_() { } void ATM90E32Component::run_gain_calibrations() { + const char *cs = this->cs_summary_.c_str(); if (!this->enable_gain_calibration_) { - ESP_LOGW(TAG, "[CALIBRATION] Gain calibration is disabled! Enable it first with enable_gain_calibration: true"); + ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true", + cs); return; } @@ -455,12 +591,14 @@ void ATM90E32Component::run_gain_calibrations() { float ref_currents[3] = {this->get_reference_current(0), this->get_reference_current(1), this->get_reference_current(2)}; - ESP_LOGI(TAG, "[CALIBRATION] "); - ESP_LOGI(TAG, "[CALIBRATION] ========================= Gain Calibration ========================="); - ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------"); - ESP_LOGI(TAG, - "[CALIBRATION] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |"); - ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------"); + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ========================= Gain Calibration =========================", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI( + TAG, + "[CALIBRATION][%s] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |", + cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); for (uint8_t phase = 0; phase < 3; phase++) { float measured_voltage = this->get_phase_voltage_avg_(phase); @@ -477,22 +615,22 @@ void ATM90E32Component::run_gain_calibrations() { // Voltage calibration if (ref_voltage <= 0.0f) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: reference voltage is 0.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: reference voltage is 0.", cs, phase_labels[phase]); } else if (measured_voltage == 0.0f) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: measured voltage is 0.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs, phase_labels[phase]); } else { uint32_t new_voltage_gain = static_cast((ref_voltage / measured_voltage) * current_voltage_gain); if (new_voltage_gain == 0) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs, phase_labels[phase]); } else { if (new_voltage_gain >= 65535) { - ESP_LOGW( - TAG, - "[CALIBRATION] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage transformer.", - phase_labels[phase]); + ESP_LOGW(TAG, + "[CALIBRATION][%s] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage " + "transformer.", + cs, phase_labels[phase]); new_voltage_gain = 65535; } this->gain_phase_[phase].voltage_gain = static_cast(new_voltage_gain); @@ -502,20 +640,20 @@ void ATM90E32Component::run_gain_calibrations() { // Current calibration if (ref_current == 0.0f) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: reference current is 0.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: reference current is 0.", cs, phase_labels[phase]); } else if (measured_current == 0.0f) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: measured current is 0.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs, phase_labels[phase]); } else { uint32_t new_current_gain = static_cast((ref_current / measured_current) * current_current_gain); if (new_current_gain == 0) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain would be 0. Check reference and measured current.", + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs, phase_labels[phase]); } else { if (new_current_gain >= 65535) { - ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.", - phase_labels[phase]); + ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.", + cs, phase_labels[phase]); new_current_gain = 65535; } this->gain_phase_[phase].current_gain = static_cast(new_current_gain); @@ -524,13 +662,13 @@ void ATM90E32Component::run_gain_calibrations() { } // Final row output - ESP_LOGI(TAG, "[CALIBRATION] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |", + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |", cs, 'A' + phase, measured_voltage, measured_current, ref_voltage, ref_current, current_voltage_gain, did_voltage ? this->gain_phase_[phase].voltage_gain : current_voltage_gain, current_current_gain, did_current ? this->gain_phase_[phase].current_gain : current_current_gain); } - ESP_LOGI(TAG, "[CALIBRATION] =====================================================================\n"); + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); this->save_gain_calibration_to_memory_(); this->write_gains_to_registers_(); @@ -538,54 +676,108 @@ void ATM90E32Component::run_gain_calibrations() { } void ATM90E32Component::save_gain_calibration_to_memory_() { + const char *cs = this->cs_summary_.c_str(); bool success = this->gain_calibration_pref_.save(&this->gain_phase_); + global_preferences->sync(); if (success) { this->using_saved_calibrations_ = true; - ESP_LOGI(TAG, "[CALIBRATION] Gain calibration saved to memory."); + ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration saved to memory.", cs); } else { this->using_saved_calibrations_ = false; - ESP_LOGE(TAG, "[CALIBRATION] Failed to save gain calibration to memory!"); + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save gain calibration to memory!", cs); + } +} + +void ATM90E32Component::save_offset_calibration_to_memory_() { + const char *cs = this->cs_summary_.c_str(); + bool success = this->offset_pref_.save(&this->offset_phase_); + global_preferences->sync(); + if (success) { + this->using_saved_calibrations_ = true; + this->restored_offset_calibration_ = true; + for (bool &phase : this->offset_calibration_mismatch_) + phase = false; + ESP_LOGI(TAG, "[CALIBRATION][%s] Offset calibration saved to memory.", cs); + } else { + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save offset calibration to memory!", cs); + } +} + +void ATM90E32Component::save_power_offset_calibration_to_memory_() { + const char *cs = this->cs_summary_.c_str(); + bool success = this->power_offset_pref_.save(&this->power_offset_phase_); + global_preferences->sync(); + if (success) { + this->using_saved_calibrations_ = true; + this->restored_power_offset_calibration_ = true; + for (bool &phase : this->power_offset_calibration_mismatch_) + phase = false; + ESP_LOGI(TAG, "[CALIBRATION][%s] Power offset calibration saved to memory.", cs); + } else { + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save power offset calibration to memory!", cs); } } void ATM90E32Component::run_offset_calibrations() { + const char *cs = this->cs_summary_.c_str(); if (!this->enable_offset_calibration_) { - ESP_LOGW(TAG, "[CALIBRATION] Offset calibration is disabled! Enable it first with enable_offset_calibration: true"); + ESP_LOGW(TAG, + "[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true", + cs); return; } + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ======================== Offset Calibration ========================", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { int16_t voltage_offset = calibrate_offset(phase, true); int16_t current_offset = calibrate_offset(phase, false); this->write_offsets_to_registers_(phase, voltage_offset, current_offset); - ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage: %d, offset_current: %d", 'A' + phase, voltage_offset, + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset, current_offset); } - this->offset_pref_.save(&this->offset_phase_); // Save to flash + ESP_LOGI(TAG, "[CALIBRATION][%s] ==================================================================\n", cs); + + this->save_offset_calibration_to_memory_(); } void ATM90E32Component::run_power_offset_calibrations() { + const char *cs = this->cs_summary_.c_str(); if (!this->enable_offset_calibration_) { ESP_LOGW( TAG, - "[CALIBRATION] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true"); + "[CALIBRATION][%s] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true", + cs); return; } + ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ===================== Power Offset Calibration =====================", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; ++phase) { int16_t active_offset = calibrate_power_offset(phase, false); int16_t reactive_offset = calibrate_power_offset(phase, true); this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset); - ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase, - active_offset, reactive_offset); + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset, + reactive_offset); } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); - this->power_offset_pref_.save(&this->power_offset_phase_); // Save to flash + this->save_power_offset_calibration_to_memory_(); } void ATM90E32Component::write_gains_to_registers_() { @@ -632,102 +824,276 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t } void ATM90E32Component::restore_gain_calibrations_() { - if (this->gain_calibration_pref_.load(&this->gain_phase_)) { - ESP_LOGI(TAG, "[CALIBRATION] Restoring saved gain calibrations to registers:"); - - for (uint8_t phase = 0; phase < 3; phase++) { - uint16_t v_gain = this->gain_phase_[phase].voltage_gain; - uint16_t i_gain = this->gain_phase_[phase].current_gain; - ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, v_gain, i_gain); - } - - this->write_gains_to_registers_(); - - if (this->verify_gain_writes_()) { - this->using_saved_calibrations_ = true; - ESP_LOGI(TAG, "[CALIBRATION] Gain calibration loaded and verified successfully."); - } else { - this->using_saved_calibrations_ = false; - ESP_LOGE(TAG, "[CALIBRATION] Gain verification failed! Calibration may not be applied correctly."); - } - } else { - this->using_saved_calibrations_ = false; - ESP_LOGW(TAG, "[CALIBRATION] No stored gain calibrations found. Using config file values."); + const char *cs = this->cs_summary_.c_str(); + for (uint8_t i = 0; i < 3; ++i) { + this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_; + this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_; + this->gain_phase_[i] = this->config_gain_phase_[i]; } + + if (this->gain_calibration_pref_.load(&this->gain_phase_)) { + bool all_zero = true; + bool same_as_config = true; + for (uint8_t phase = 0; phase < 3; ++phase) { + const auto &cfg = this->config_gain_phase_[phase]; + const auto &saved = this->gain_phase_[phase]; + if (saved.voltage_gain != 0 || saved.current_gain != 0) + all_zero = false; + if (saved.voltage_gain != cfg.voltage_gain || saved.current_gain != cfg.current_gain) + same_as_config = false; + } + + if (!all_zero && !same_as_config) { + for (uint8_t phase = 0; phase < 3; ++phase) { + bool mismatch = false; + if (this->has_config_voltage_gain_[phase] && + this->gain_phase_[phase].voltage_gain != this->config_gain_phase_[phase].voltage_gain) + mismatch = true; + if (this->has_config_current_gain_[phase] && + this->gain_phase_[phase].current_gain != this->config_gain_phase_[phase].current_gain) + mismatch = true; + if (mismatch) + this->gain_calibration_mismatch_[phase] = true; + } + + this->write_gains_to_registers_(); + + if (this->verify_gain_writes_()) { + this->using_saved_calibrations_ = true; + this->restored_gain_calibration_ = true; + return; + } + + this->using_saved_calibrations_ = false; + ESP_LOGE(TAG, "[CALIBRATION][%s] Gain verification failed! Calibration may not be applied correctly.", cs); + } + } + + this->using_saved_calibrations_ = false; + for (uint8_t i = 0; i < 3; ++i) + this->gain_phase_[i] = this->config_gain_phase_[i]; + this->write_gains_to_registers_(); + + ESP_LOGW(TAG, "[CALIBRATION][%s] No stored gain calibrations found. Using config file values.", cs); } void ATM90E32Component::restore_offset_calibrations_() { - if (this->offset_pref_.load(&this->offset_phase_)) { - ESP_LOGI(TAG, "[CALIBRATION] Successfully restored offset calibration from memory."); + const char *cs = this->cs_summary_.c_str(); + for (uint8_t i = 0; i < 3; ++i) + this->config_offset_phase_[i] = this->offset_phase_[i]; + bool have_data = this->offset_pref_.load(&this->offset_phase_); + bool all_zero = true; + if (have_data) { + for (auto &phase : this->offset_phase_) { + if (phase.voltage_offset_ != 0 || phase.current_offset_ != 0) { + all_zero = false; + break; + } + } + } + + if (have_data && !all_zero) { + this->restored_offset_calibration_ = true; for (uint8_t phase = 0; phase < 3; phase++) { auto &offset = this->offset_phase_[phase]; - write_offsets_to_registers_(phase, offset.voltage_offset_, offset.current_offset_); - ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage:: %d, offset_current: %d", 'A' + phase, - offset.voltage_offset_, offset.current_offset_); + bool mismatch = false; + if (this->has_config_voltage_offset_[phase] && + offset.voltage_offset_ != this->config_offset_phase_[phase].voltage_offset_) + mismatch = true; + if (this->has_config_current_offset_[phase] && + offset.current_offset_ != this->config_offset_phase_[phase].current_offset_) + mismatch = true; + if (mismatch) + this->offset_calibration_mismatch_[phase] = true; } } else { - ESP_LOGW(TAG, "[CALIBRATION] No stored offset calibrations found. Using default values."); + for (uint8_t phase = 0; phase < 3; phase++) + this->offset_phase_[phase] = this->config_offset_phase_[phase]; + ESP_LOGW(TAG, "[CALIBRATION][%s] No stored offset calibrations found. Using default values.", cs); + } + + for (uint8_t phase = 0; phase < 3; phase++) { + write_offsets_to_registers_(phase, this->offset_phase_[phase].voltage_offset_, + this->offset_phase_[phase].current_offset_); } } void ATM90E32Component::restore_power_offset_calibrations_() { - if (this->power_offset_pref_.load(&this->power_offset_phase_)) { - ESP_LOGI(TAG, "[CALIBRATION] Successfully restored power offset calibration from memory."); + const char *cs = this->cs_summary_.c_str(); + for (uint8_t i = 0; i < 3; ++i) + this->config_power_offset_phase_[i] = this->power_offset_phase_[i]; + bool have_data = this->power_offset_pref_.load(&this->power_offset_phase_); + bool all_zero = true; + if (have_data) { + for (auto &phase : this->power_offset_phase_) { + if (phase.active_power_offset != 0 || phase.reactive_power_offset != 0) { + all_zero = false; + break; + } + } + } + + if (have_data && !all_zero) { + this->restored_power_offset_calibration_ = true; for (uint8_t phase = 0; phase < 3; ++phase) { auto &offset = this->power_offset_phase_[phase]; - write_power_offsets_to_registers_(phase, offset.active_power_offset, offset.reactive_power_offset); - ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase, - offset.active_power_offset, offset.reactive_power_offset); + bool mismatch = false; + if (this->has_config_active_power_offset_[phase] && + offset.active_power_offset != this->config_power_offset_phase_[phase].active_power_offset) + mismatch = true; + if (this->has_config_reactive_power_offset_[phase] && + offset.reactive_power_offset != this->config_power_offset_phase_[phase].reactive_power_offset) + mismatch = true; + if (mismatch) + this->power_offset_calibration_mismatch_[phase] = true; } } else { - ESP_LOGW(TAG, "[CALIBRATION] No stored power offsets found. Using default values."); + for (uint8_t phase = 0; phase < 3; ++phase) + this->power_offset_phase_[phase] = this->config_power_offset_phase_[phase]; + ESP_LOGW(TAG, "[CALIBRATION][%s] No stored power offsets found. Using default values.", cs); + } + + for (uint8_t phase = 0; phase < 3; ++phase) { + write_power_offsets_to_registers_(phase, this->power_offset_phase_[phase].active_power_offset, + this->power_offset_phase_[phase].reactive_power_offset); } } void ATM90E32Component::clear_gain_calibrations() { - ESP_LOGI(TAG, "[CALIBRATION] Clearing stored gain calibrations and restoring config-defined values"); - - for (int phase = 0; phase < 3; phase++) { - gain_phase_[phase].voltage_gain = this->phase_[phase].voltage_gain_; - gain_phase_[phase].current_gain = this->phase_[phase].ct_gain_; + const char *cs = this->cs_summary_.c_str(); + if (!this->using_saved_calibrations_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); + for (int phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase, + this->gain_phase_[phase].voltage_gain, this->gain_phase_[phase].current_gain); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs); + return; } - bool success = this->gain_calibration_pref_.save(&this->gain_phase_); - this->using_saved_calibrations_ = false; + ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored gain calibrations and restoring config-defined values", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs); - if (success) { - ESP_LOGI(TAG, "[CALIBRATION] Gain calibrations cleared. Config values restored:"); - for (int phase = 0; phase < 3; phase++) { - ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, - gain_phase_[phase].voltage_gain, gain_phase_[phase].current_gain); - } - } else { - ESP_LOGE(TAG, "[CALIBRATION] Failed to clear gain calibrations!"); + for (int phase = 0; phase < 3; phase++) { + uint16_t voltage_gain = this->phase_[phase].voltage_gain_; + uint16_t current_gain = this->phase_[phase].ct_gain_; + + this->config_gain_phase_[phase].voltage_gain = voltage_gain; + this->config_gain_phase_[phase].current_gain = current_gain; + this->gain_phase_[phase].voltage_gain = voltage_gain; + this->gain_phase_[phase].current_gain = current_gain; + + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase, voltage_gain, current_gain); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs); + + GainCalibration zero_gains[3]{{0, 0}, {0, 0}, {0, 0}}; + bool success = this->gain_calibration_pref_.save(&zero_gains); + global_preferences->sync(); + + this->using_saved_calibrations_ = false; + this->restored_gain_calibration_ = false; + for (bool &phase : this->gain_calibration_mismatch_) + phase = false; + + if (!success) { + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to clear gain calibrations!", cs); } this->write_gains_to_registers_(); // Apply them to the chip immediately } void ATM90E32Component::clear_offset_calibrations() { - for (uint8_t phase = 0; phase < 3; phase++) { - this->write_offsets_to_registers_(phase, 0, 0); + const char *cs = this->cs_summary_.c_str(); + if (!this->restored_offset_calibration_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, + this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs); + return; } - this->offset_pref_.save(&this->offset_phase_); // Save cleared values to flash memory + ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored offset calibrations and restoring config-defined values", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); - ESP_LOGI(TAG, "[CALIBRATION] Offsets cleared."); + for (uint8_t phase = 0; phase < 3; phase++) { + int16_t voltage_offset = + this->has_config_voltage_offset_[phase] ? this->config_offset_phase_[phase].voltage_offset_ : 0; + int16_t current_offset = + this->has_config_current_offset_[phase] ? this->config_offset_phase_[phase].current_offset_ : 0; + this->write_offsets_to_registers_(phase, voltage_offset, current_offset); + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset, + current_offset); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs); + + OffsetCalibration zero_offsets[3]{{0, 0}, {0, 0}, {0, 0}}; + this->offset_pref_.save(&zero_offsets); // Clear stored values in flash + global_preferences->sync(); + + this->restored_offset_calibration_ = false; + for (bool &phase : this->offset_calibration_mismatch_) + phase = false; + + ESP_LOGI(TAG, "[CALIBRATION][%s] Offsets cleared.", cs); } void ATM90E32Component::clear_power_offset_calibrations() { - for (uint8_t phase = 0; phase < 3; phase++) { - this->write_power_offsets_to_registers_(phase, 0, 0); + const char *cs = this->cs_summary_.c_str(); + if (!this->restored_power_offset_calibration_) { + ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + for (uint8_t phase = 0; phase < 3; phase++) { + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, + this->power_offset_phase_[phase].active_power_offset, + this->power_offset_phase_[phase].reactive_power_offset); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); + return; } - this->power_offset_pref_.save(&this->power_offset_phase_); + ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored power offsets and restoring config-defined values", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); + ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); - ESP_LOGI(TAG, "[CALIBRATION] Power offsets cleared."); + for (uint8_t phase = 0; phase < 3; phase++) { + int16_t active_offset = + this->has_config_active_power_offset_[phase] ? this->config_power_offset_phase_[phase].active_power_offset : 0; + int16_t reactive_offset = this->has_config_reactive_power_offset_[phase] + ? this->config_power_offset_phase_[phase].reactive_power_offset + : 0; + this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset); + ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset, + reactive_offset); + } + ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); + + PowerOffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}}; + this->power_offset_pref_.save(&zero_power_offsets); + global_preferences->sync(); + + this->restored_power_offset_calibration_ = false; + for (bool &phase : this->power_offset_calibration_mismatch_) + phase = false; + + ESP_LOGI(TAG, "[CALIBRATION][%s] Power offsets cleared.", cs); } int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) { @@ -748,20 +1114,21 @@ int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) { int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) { const uint8_t num_reads = 5; - uint64_t total_value = 0; + int64_t total_value = 0; for (uint8_t i = 0; i < num_reads; ++i) { - uint32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase) - : this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase); + int32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase) + : this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase); total_value += reading; } - const uint32_t average_value = total_value / num_reads; - const uint32_t power_offset = ~average_value + 1; + int32_t average_value = total_value / num_reads; + int32_t power_offset = -average_value; return static_cast(power_offset); // Takes the lower 16 bits } bool ATM90E32Component::verify_gain_writes_() { + const char *cs = this->cs_summary_.c_str(); bool success = true; for (uint8_t phase = 0; phase < 3; phase++) { uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]); @@ -769,7 +1136,7 @@ bool ATM90E32Component::verify_gain_writes_() { if (read_voltage != this->gain_phase_[phase].voltage_gain || read_current != this->gain_phase_[phase].current_gain) { - ESP_LOGE(TAG, "[CALIBRATION] Mismatch detected for Phase %s!", phase_labels[phase]); + ESP_LOGE(TAG, "[CALIBRATION][%s] Mismatch detected for Phase %s!", cs, phase_labels[phase]); success = false; } } @@ -792,16 +1159,16 @@ void ATM90E32Component::check_phase_status() { status += "Phase Loss; "; auto *sensor = this->phase_status_text_sensor_[phase]; - const char *phase_name = sensor ? sensor->get_name().c_str() : "Unknown Phase"; + if (sensor == nullptr) + continue; + if (!status.empty()) { status.pop_back(); // remove space status.pop_back(); // remove semicolon - ESP_LOGW(TAG, "%s: %s", phase_name, status.c_str()); - if (sensor != nullptr) - sensor->publish_state(status); + ESP_LOGW(TAG, "%s: %s", sensor->get_name().c_str(), status.c_str()); + sensor->publish_state(status); } else { - if (sensor != nullptr) - sensor->publish_state("Okay"); + sensor->publish_state("Okay"); } } } @@ -818,9 +1185,12 @@ void ATM90E32Component::check_freq_status() { } else { freq_status = "Normal"; } - ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str()); - if (this->freq_status_text_sensor_ != nullptr) { + if (freq_status == "Normal") { + ESP_LOGD(TAG, "Frequency status: %s", freq_status.c_str()); + } else { + ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str()); + } this->freq_status_text_sensor_->publish_state(freq_status); } } diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 0703c40ae0..afbd9cf941 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -61,15 +61,29 @@ class ATM90E32Component : public PollingComponent, this->phase_[phase].harmonic_active_power_sensor_ = obj; } void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; } - void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; } - void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } - void set_voltage_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].voltage_offset_ = offset; } - void set_current_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].current_offset_ = offset; } + void set_volt_gain(int phase, uint16_t gain) { + this->phase_[phase].voltage_gain_ = gain; + this->has_config_voltage_gain_[phase] = true; + } + void set_ct_gain(int phase, uint16_t gain) { + this->phase_[phase].ct_gain_ = gain; + this->has_config_current_gain_[phase] = true; + } + void set_voltage_offset(uint8_t phase, int16_t offset) { + this->offset_phase_[phase].voltage_offset_ = offset; + this->has_config_voltage_offset_[phase] = true; + } + void set_current_offset(uint8_t phase, int16_t offset) { + this->offset_phase_[phase].current_offset_ = offset; + this->has_config_current_offset_[phase] = true; + } void set_active_power_offset(uint8_t phase, int16_t offset) { this->power_offset_phase_[phase].active_power_offset = offset; + this->has_config_active_power_offset_[phase] = true; } void set_reactive_power_offset(uint8_t phase, int16_t offset) { this->power_offset_phase_[phase].reactive_power_offset = offset; + this->has_config_reactive_power_offset_[phase] = true; } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; } @@ -126,8 +140,9 @@ class ATM90E32Component : public PollingComponent, number::Number *ref_currents_[3]{nullptr, nullptr, nullptr}; #endif uint16_t read16_(uint16_t a_register); + uint16_t read16_transaction_(uint16_t a_register); int read32_(uint16_t addr_h, uint16_t addr_l); - void write16_(uint16_t a_register, uint16_t val); + void write16_(uint16_t a_register, uint16_t val, bool validate = true); float get_local_phase_voltage_(uint8_t phase); float get_local_phase_current_(uint8_t phase); float get_local_phase_active_power_(uint8_t phase); @@ -159,12 +174,15 @@ class ATM90E32Component : public PollingComponent, void restore_offset_calibrations_(); void restore_power_offset_calibrations_(); void restore_gain_calibrations_(); + void save_offset_calibration_to_memory_(); void save_gain_calibration_to_memory_(); + void save_power_offset_calibration_to_memory_(); void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset); void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset); void write_gains_to_registers_(); bool verify_gain_writes_(); bool validate_spi_read_(uint16_t expected, const char *context = nullptr); + void log_calibration_status_(); struct ATM90E32Phase { uint16_t voltage_gain_{0}; @@ -204,19 +222,33 @@ class ATM90E32Component : public PollingComponent, int16_t current_offset_{0}; } offset_phase_[3]; + OffsetCalibration config_offset_phase_[3]; + struct PowerOffsetCalibration { int16_t active_power_offset{0}; int16_t reactive_power_offset{0}; } power_offset_phase_[3]; + PowerOffsetCalibration config_power_offset_phase_[3]; + struct GainCalibration { uint16_t voltage_gain{1}; uint16_t current_gain{1}; } gain_phase_[3]; + GainCalibration config_gain_phase_[3]; + + bool has_config_voltage_offset_[3]{false, false, false}; + bool has_config_current_offset_[3]{false, false, false}; + bool has_config_active_power_offset_[3]{false, false, false}; + bool has_config_reactive_power_offset_[3]{false, false, false}; + bool has_config_voltage_gain_[3]{false, false, false}; + bool has_config_current_gain_[3]{false, false, false}; + ESPPreferenceObject offset_pref_; ESPPreferenceObject power_offset_pref_; ESPPreferenceObject gain_calibration_pref_; + std::string cs_summary_; sensor::Sensor *freq_sensor_{nullptr}; #ifdef USE_TEXT_SENSOR @@ -231,6 +263,13 @@ class ATM90E32Component : public PollingComponent, bool peak_current_signed_{false}; bool enable_offset_calibration_{false}; bool enable_gain_calibration_{false}; + bool restored_offset_calibration_{false}; + bool restored_power_offset_calibration_{false}; + bool restored_gain_calibration_{false}; + bool calibration_message_printed_{false}; + bool offset_calibration_mismatch_[3]{false, false, false}; + bool power_offset_calibration_mismatch_[3]{false, false, false}; + bool gain_calibration_mismatch_[3]{false, false, false}; }; } // namespace atm90e32 diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index 95c31872e3..e01d7eb101 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -15,7 +15,7 @@ class AudioStreamInfo { * - An audio sample represents a unit of audio for one channel. * - A frame represents a unit of audio with a sample for every channel. * - * In gneneral, converting between bytes, samples, and frames shouldn't result in rounding errors so long as frames + * In general, converting between bytes, samples, and frames shouldn't result in rounding errors so long as frames * are used as the main unit when transferring audio data. Durations may result in rounding for certain sample rates; * e.g., 44.1 KHz. The ``frames_to_milliseconds_with_remainder`` function should be used for accuracy, as it takes * into account the remainder rather than just ignoring any rounding. @@ -76,7 +76,7 @@ class AudioStreamInfo { /// @brief Computes the duration, in microseconds, the given amount of frames represents. /// @param frames Number of audio frames - /// @return Duration in microseconds `frames` respresents. May be slightly inaccurate due to integer divison rounding + /// @return Duration in microseconds `frames` represents. May be slightly inaccurate due to integer division rounding /// for certain sample rates. uint32_t frames_to_microseconds(uint32_t frames) const; diff --git a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp index e6e049e332..4adf0bbbe0 100644 --- a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +++ b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp @@ -17,7 +17,6 @@ constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a, } void AXS15231Touchscreen::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (this->reset_pin_ != nullptr) { this->reset_pin_->setup(); this->reset_pin_->digital_write(false); @@ -36,7 +35,6 @@ void AXS15231Touchscreen::setup() { if (this->y_raw_max_ == 0) { this->y_raw_max_ = this->display_->get_native_height(); } - ESP_LOGCONFIG(TAG, "AXS15231 Touchscreen setup complete"); } void AXS15231Touchscreen::update_touches() { diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp index 17b2dd1808..67b8472257 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.cpp +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -121,8 +121,6 @@ void spi_dma_tx_finish_callback(unsigned int param) { } void BekenSPILEDStripLightOutput::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - size_t buffer_size = this->get_buffer_size_(); size_t dma_buffer_size = (buffer_size * 8) + (2 * 64); diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 267a728fdd..2fc476c17d 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -38,7 +38,6 @@ MTreg: */ void BH1750Sensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); uint8_t turn_on = BH1750_COMMAND_POWER_ON; if (this->write(&turn_on, 1) != i2c::ERROR_OK) { this->mark_failed(); diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c97de6d5e5..b56fde1ffd 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -266,8 +266,10 @@ async def delayed_off_filter_to_code(config, filter_id): async def autorepeat_filter_to_code(config, filter_id): timings = [] if len(config) > 0: - for conf in config: - timings.append((conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])) + timings.extend( + (conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON]) + for conf in config + ) else: timings.append( ( @@ -514,6 +516,7 @@ def binary_sensor_schema( icon: str = cv.UNDEFINED, entity_category: str = cv.UNDEFINED, device_class: str = cv.UNDEFINED, + filters: list = cv.UNDEFINED, ) -> cv.Schema: schema = {} @@ -525,6 +528,7 @@ def binary_sensor_schema( (CONF_ICON, icon, cv.icon), (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), (CONF_DEVICE_CLASS, device_class, validate_device_class), + (CONF_FILTERS, filters, validate_filters), ]: if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator @@ -573,16 +577,15 @@ async def setup_binary_sensor_core_(var, config): await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_MULTI_CLICK, []): - timings = [] - for tim in conf[CONF_TIMING]: - timings.append( - cg.StructInitializer( - MultiClickTriggerEvent, - ("state", tim[CONF_STATE]), - ("min_length", tim[CONF_MIN_LENGTH]), - ("max_length", tim.get(CONF_MAX_LENGTH, 4294967294)), - ) + timings = [ + cg.StructInitializer( + MultiClickTriggerEvent, + ("state", tim[CONF_STATE]), + ("min_length", tim[CONF_MIN_LENGTH]), + ("max_length", tim.get(CONF_MAX_LENGTH, 4294967294)), ) + for tim in conf[CONF_TIMING] + ] trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings) if CONF_INVALID_COOLDOWN in conf: cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN])) @@ -651,7 +654,6 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args) @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_BINARY_SENSOR") cg.add_global(binary_sensor_ns.using) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index a88172ca87..5f4ea8afd1 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -175,8 +175,7 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema( ) async def ble_disconnect_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, parent) - return var + return cg.new_Pvariable(action_id, template_arg, parent) @automation.register_action( @@ -184,8 +183,7 @@ async def ble_disconnect_to_code(config, action_id, template_arg, args): ) async def ble_connect_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, parent) - return var + return cg.new_Pvariable(action_id, template_arg, parent) @automation.register_action( @@ -282,14 +280,13 @@ async def passkey_reply_to_code(config, action_id, template_arg, args): ) async def remove_bond_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, parent) - - return var + return cg.new_Pvariable(action_id, template_arg, parent) async def to_code(config): # Register the loggers this component needs esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP) + cg.add_define("USE_ESP32_BLE_UUID") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index 5c144cadcc..112faa27e5 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -1,13 +1,19 @@ +import logging + import esphome.codegen as cg from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32_ble import BTLoggers import esphome.config_validation as cv from esphome.const import CONF_ACTIVE, CONF_ID +from esphome.core import CORE +from esphome.log import AnsiFore, color AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"] DEPENDENCIES = ["api", "esp32"] -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@bdraco"] + +_LOGGER = logging.getLogger(__name__) CONF_CONNECTION_SLOTS = "connection_slots" CONF_CACHE_SERVICES = "cache_services" @@ -41,6 +47,27 @@ def validate_connections(config): esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")( config ) + + # Warn about connection slot waste when using Arduino framework + if CORE.using_arduino and connection_slots: + _LOGGER.warning( + "Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n" + "If BLE connections fail, they can waste connection slots for 10 seconds because\n" + "Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n" + "ESP-IDF framework allows setting it to 20s to match client timeouts.\n" + "\n" + "To switch to ESP-IDF, add this to your YAML:\n" + " esp32:\n" + " framework:\n" + " type: esp-idf\n" + "\n" + "For detailed migration instructions, see:\n" + "%s", + color( + AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html" + ), + ) + return { **config, CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)], @@ -85,13 +112,23 @@ async def to_code(config): await cg.register_component(var, config) cg.add(var.set_active(config[CONF_ACTIVE])) - await esp32_ble_tracker.register_ble_device(var, config) + await esp32_ble_tracker.register_raw_ble_device(var, config) + + # Define max connections for protobuf fixed array + connection_count = len(config.get(CONF_CONNECTIONS, [])) + cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count) + + # Define batch size for BLE advertisements + # Each advertisement is up to 80 bytes when packaged (including protocol overhead) + # 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload + # This achieves ~97% WiFi MTU utilization while staying under the limit + cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16) for connection_conf in config.get(CONF_CONNECTIONS, []): connection_var = cg.new_Pvariable(connection_conf[CONF_ID]) await cg.register_component(connection_var, connection_conf) cg.add(var.register_connection(connection_var)) - await esp32_ble_tracker.register_client(connection_var, connection_conf) + await esp32_ble_tracker.register_raw_client(connection_var, connection_conf) if config.get(CONF_CACHE_SERVICES): add_idf_sdkconfig_option("CONFIG_BT_GATTC_CACHE_NVS_FLASH", True) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 44d434802c..347f60c28f 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -8,16 +8,369 @@ #include "bluetooth_proxy.h" -namespace esphome { -namespace bluetooth_proxy { +namespace esphome::bluetooth_proxy { static const char *const TAG = "bluetooth_proxy.connection"; +// This function is allocation-free and directly packs UUIDs into the output array +// using precalculated constants for the Bluetooth base UUID +static void fill_128bit_uuid_array(std::array &out, esp_bt_uuid_t uuid_source) { + // Bluetooth base UUID: 00000000-0000-1000-8000-00805F9B34FB + // out[0] = bytes 8-15 (big-endian) + // - For 128-bit UUIDs: use bytes 8-15 as-is + // - For 16/32-bit UUIDs: insert into bytes 12-15, use 0x00001000 for bytes 8-11 + out[0] = uuid_source.len == ESP_UUID_LEN_128 + ? (((uint64_t) uuid_source.uuid.uuid128[15] << 56) | ((uint64_t) uuid_source.uuid.uuid128[14] << 48) | + ((uint64_t) uuid_source.uuid.uuid128[13] << 40) | ((uint64_t) uuid_source.uuid.uuid128[12] << 32) | + ((uint64_t) uuid_source.uuid.uuid128[11] << 24) | ((uint64_t) uuid_source.uuid.uuid128[10] << 16) | + ((uint64_t) uuid_source.uuid.uuid128[9] << 8) | ((uint64_t) uuid_source.uuid.uuid128[8])) + : (((uint64_t) (uuid_source.len == ESP_UUID_LEN_16 ? uuid_source.uuid.uuid16 : uuid_source.uuid.uuid32) + << 32) | + 0x00001000ULL); // Base UUID bytes 8-11 + // out[1] = bytes 0-7 (big-endian) + // - For 128-bit UUIDs: use bytes 0-7 as-is + // - For 16/32-bit UUIDs: use precalculated base UUID constant + out[1] = uuid_source.len == ESP_UUID_LEN_128 + ? ((uint64_t) uuid_source.uuid.uuid128[7] << 56) | ((uint64_t) uuid_source.uuid.uuid128[6] << 48) | + ((uint64_t) uuid_source.uuid.uuid128[5] << 40) | ((uint64_t) uuid_source.uuid.uuid128[4] << 32) | + ((uint64_t) uuid_source.uuid.uuid128[3] << 24) | ((uint64_t) uuid_source.uuid.uuid128[2] << 16) | + ((uint64_t) uuid_source.uuid.uuid128[1] << 8) | ((uint64_t) uuid_source.uuid.uuid128[0]) + : 0x800000805F9B34FBULL; // Base UUID bytes 0-7: 80-00-00-80-5F-9B-34-FB +} + +// Helper to fill UUID in the appropriate format based on client support and UUID type +static void fill_gatt_uuid(std::array &uuid_128, uint32_t &short_uuid, const esp_bt_uuid_t &uuid, + bool use_efficient_uuids) { + if (!use_efficient_uuids || uuid.len == ESP_UUID_LEN_128) { + // Use 128-bit format for old clients or when UUID is already 128-bit + fill_128bit_uuid_array(uuid_128, uuid); + } else if (uuid.len == ESP_UUID_LEN_16) { + short_uuid = uuid.uuid.uuid16; + } else if (uuid.len == ESP_UUID_LEN_32) { + short_uuid = uuid.uuid.uuid32; + } +} + +// Constants for size estimation +static constexpr uint8_t SERVICE_OVERHEAD_LEGACY = 25; // UUID(20) + handle(4) + overhead(1) +static constexpr uint8_t SERVICE_OVERHEAD_EFFICIENT = 10; // UUID(6) + handle(4) +static constexpr uint8_t CHAR_SIZE_128BIT = 35; // UUID(20) + handle(4) + props(4) + overhead(7) +static constexpr uint8_t DESC_SIZE_128BIT = 25; // UUID(20) + handle(4) + overhead(1) +static constexpr uint8_t DESC_SIZE_16BIT = 10; // UUID(6) + handle(4) +static constexpr uint8_t DESC_PER_CHAR = 1; // Assume 1 descriptor per characteristic + +// Helper to estimate service size before fetching all data +/** + * Estimate the size of a Bluetooth service based on the number of characteristics and UUID format. + * + * @param char_count The number of characteristics in the service. + * @param use_efficient_uuids Whether to use efficient UUIDs (16-bit or 32-bit) for newer APIVersions. + * @return The estimated size of the service in bytes. + * + * This function calculates the size of a Bluetooth service by considering: + * - A service overhead, which depends on whether efficient UUIDs are used. + * - The size of each characteristic, assuming 128-bit UUIDs for safety. + * - The size of descriptors, assuming one 128-bit descriptor per characteristic. + */ +static size_t estimate_service_size(uint16_t char_count, bool use_efficient_uuids) { + size_t service_overhead = use_efficient_uuids ? SERVICE_OVERHEAD_EFFICIENT : SERVICE_OVERHEAD_LEGACY; + // Always assume 128-bit UUIDs for characteristics to be safe + size_t char_size = CHAR_SIZE_128BIT; + // Assume one 128-bit descriptor per characteristic + size_t desc_size = DESC_SIZE_128BIT * DESC_PER_CHAR; + + return service_overhead + (char_size + desc_size) * char_count; +} + +bool BluetoothConnection::supports_efficient_uuids_() const { + auto *api_conn = this->proxy_->get_api_connection(); + return api_conn && api_conn->client_supports_api_version(1, 12); +} + void BluetoothConnection::dump_config() { ESP_LOGCONFIG(TAG, "BLE Connection:"); BLEClientBase::dump_config(); } +void BluetoothConnection::update_allocated_slot_(uint64_t find_value, uint64_t set_value) { + auto &allocated = this->proxy_->connections_free_response_.allocated; + for (auto &slot : allocated) { + if (slot == find_value) { + slot = set_value; + return; + } + } +} + +void BluetoothConnection::set_address(uint64_t address) { + // If we're clearing an address (disconnecting), update the pre-allocated message + if (address == 0 && this->address_ != 0) { + this->proxy_->connections_free_response_.free++; + this->update_allocated_slot_(this->address_, 0); + } + // If we're setting a new address (connecting), update the pre-allocated message + else if (address != 0 && this->address_ == 0) { + this->proxy_->connections_free_response_.free--; + this->update_allocated_slot_(0, address); + } + + // Call parent implementation to actually set the address + BLEClientBase::set_address(address); +} + +void BluetoothConnection::loop() { + BLEClientBase::loop(); + + // Early return if no active connection + if (this->address_ == 0) { + return; + } + + // Handle service discovery if in valid range + if (this->send_service_ >= 0 && this->send_service_ <= this->service_count_) { + this->send_service_for_discovery_(); + } + + // Check if we should disable the loop + // - For V3_WITH_CACHE: Services are never sent, disable after INIT state + // - For other connections: Disable only after service discovery is complete + // (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent) + if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || + this->send_service_ == DONE_SENDING_SERVICES)) { + this->disable_loop(); + } +} + +void BluetoothConnection::reset_connection_(esp_err_t reason) { + // 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_ = INIT_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) { + this->send_service_ = DONE_SENDING_SERVICES; + return; + } + + // Check if client supports efficient UUIDs + bool use_efficient_uuids = this->supports_efficient_uuids_(); + + // Prepare response + api::BluetoothGATTGetServicesResponse resp; + resp.address = this->address_; + + // Dynamic batching based on actual size + // Conservative MTU limit for API messages (accounts for WPA3 overhead) + static constexpr size_t MAX_PACKET_SIZE = 1360; + + // Keep running total of actual message size + size_t current_size = 0; + api::ProtoSize size; + resp.calculate_size(size); + current_size = size.get_size(); + + while (this->send_service_ < this->service_count_) { + 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_); + + if (service_status != ESP_GATT_OK || service_count == 0) { + ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d", + this->connection_index_, this->address_str().c_str(), + service_status != ESP_GATT_OK ? "error" : "missing", service_status, service_count, this->send_service_); + this->send_service_ = DONE_SENDING_SERVICES; + return; + } + + // Get the number of characteristics BEFORE adding to response + 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) { + this->log_connection_error_("esp_ble_gattc_get_attr_count", char_count_status); + this->send_service_ = DONE_SENDING_SERVICES; + return; + } + + // If this service likely won't fit, send current batch (unless it's the first) + size_t estimated_size = estimate_service_size(total_char_count, use_efficient_uuids); + if (!resp.services.empty() && (current_size + estimated_size > MAX_PACKET_SIZE)) { + // This service likely won't fit, send current batch + break; + } + + // Now add the service since we know it will likely fit + resp.services.emplace_back(); + auto &service_resp = resp.services.back(); + + fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, service_result.uuid, use_efficient_uuids); + + service_resp.handle = service_result.start_handle; + + if (total_char_count > 0) { + // Reserve space and process characteristics + service_resp.characteristics.reserve(total_char_count); + 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) { + this->log_connection_error_("esp_ble_gattc_get_all_char", char_status); + this->send_service_ = DONE_SENDING_SERVICES; + return; + } + if (char_count == 0) { + break; + } + + service_resp.characteristics.emplace_back(); + auto &characteristic_resp = service_resp.characteristics.back(); + + fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids); + + characteristic_resp.handle = char_result.char_handle; + characteristic_resp.properties = char_result.properties; + char_offset++; + + // 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, 0, 0, char_result.char_handle, &total_desc_count); + + if (desc_count_status != ESP_GATT_OK) { + this->log_connection_error_("esp_ble_gattc_get_attr_count", desc_count_status); + this->send_service_ = DONE_SENDING_SERVICES; + return; + } + if (total_desc_count == 0) { + // No descriptors, continue to next characteristic + continue; + } + + // Reserve space and process descriptors + characteristic_resp.descriptors.reserve(total_desc_count); + 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) { + this->log_connection_error_("esp_ble_gattc_get_all_descr", desc_status); + this->send_service_ = DONE_SENDING_SERVICES; + return; + } + if (desc_count == 0) { + break; // No more descriptors + } + + characteristic_resp.descriptors.emplace_back(); + auto &descriptor_resp = characteristic_resp.descriptors.back(); + + fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids); + + descriptor_resp.handle = desc_result.handle; + desc_offset++; + } + } + } // end if (total_char_count > 0) + + // Calculate the actual size of just this service + api::ProtoSize service_sizer; + service_resp.calculate_size(service_sizer); + size_t service_size = service_sizer.get_size() + 1; // +1 for field tag + + // Check if adding this service would exceed the limit + if (current_size + service_size > MAX_PACKET_SIZE) { + // We would go over - pop the last service if we have more than one + if (resp.services.size() > 1) { + resp.services.pop_back(); + ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch", + this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size, + MAX_PACKET_SIZE); + // Don't increment send_service_ - we'll retry this service in next batch + } else { + // This single service is too large, but we have to send it anyway + ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_, + this->address_str().c_str(), this->send_service_, service_size); + // Increment so we don't get stuck + this->send_service_++; + } + // Send what we have + break; + } + + // Now we know we're keeping this service, add its size + current_size += service_size; + // Successfully added this service, increment counter + this->send_service_++; + } + + // Send the message with dynamically batched services + api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); +} + +void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) { + ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str().c_str(), operation, + status); +} + +void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) { + ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str().c_str(), operation, err); +} + +void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) { + ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str().c_str(), + action, type); +} + +void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) { + ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str().c_str(), + operation, handle, status); +} + +esp_err_t BluetoothConnection::check_and_log_error_(const char *operation, esp_err_t err) { + if (err != ESP_OK) { + this->log_connection_warning_(operation, err); + return err; + } + return ESP_OK; +} + bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { if (!BLEClientBase::gattc_event_handler(event, gattc_if, param)) @@ -25,22 +378,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(); @@ -64,59 +411,54 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga case ESP_GATTC_READ_DESCR_EVT: case ESP_GATTC_READ_CHAR_EVT: { if (param->read.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_, - this->address_str_.c_str(), param->read.handle, param->read.status); + this->log_gatt_operation_error_("reading char/descriptor", param->read.handle, param->read.status); this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); break; } api::BluetoothGATTReadResponse resp; resp.address = this->address_; resp.handle = param->read.handle; - 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); + resp.set_data(param->read.value, param->read.value_len); + this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); break; } case ESP_GATTC_WRITE_CHAR_EVT: case ESP_GATTC_WRITE_DESCR_EVT: { if (param->write.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_, - this->address_str_.c_str(), param->write.handle, param->write.status); + this->log_gatt_operation_error_("writing char/descriptor", param->write.handle, param->write.status); this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); break; } 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: { if (param->unreg_for_notify.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d", - this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle, - param->unreg_for_notify.status); + this->log_gatt_operation_error_("unregistering notifications", param->unreg_for_notify.handle, + param->unreg_for_notify.status); this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); break; } 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: { if (param->reg_for_notify.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_, - this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status); + this->log_gatt_operation_error_("registering notifications", param->reg_for_notify.handle, + param->reg_for_notify.status); this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); break; } 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: { @@ -125,10 +467,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga api::BluetoothGATTNotifyDataResponse resp; resp.address = this->address_; resp.handle = param->notify.handle; - 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); + resp.set_data(param->notify.value, param->notify.value_len); + this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); break; } default: @@ -157,8 +497,7 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) { if (!this->connected()) { - ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT characteristic, not connected.", this->connection_index_, - this->address_str_.c_str()); + this->log_gatt_not_connected_("read", "characteristic"); return ESP_GATT_NOT_CONNECTED; } @@ -166,18 +505,12 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) { handle); esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); - if (err != ERR_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char error, err=%d", this->connection_index_, - this->address_str_.c_str(), err); - return err; - } - return ESP_OK; + return this->check_and_log_error_("esp_ble_gattc_read_char", err); } esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) { if (!this->connected()) { - ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT characteristic, not connected.", this->connection_index_, - this->address_str_.c_str()); + this->log_gatt_not_connected_("write", "characteristic"); return ESP_GATT_NOT_CONNECTED; } ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(), @@ -186,36 +519,24 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std:: esp_err_t err = esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(), response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); - if (err != ERR_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char error, err=%d", this->connection_index_, - this->address_str_.c_str(), err); - return err; - } - return ESP_OK; + return this->check_and_log_error_("esp_ble_gattc_write_char", err); } esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) { if (!this->connected()) { - ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT descriptor, not connected.", this->connection_index_, - this->address_str_.c_str()); + this->log_gatt_not_connected_("read", "descriptor"); return ESP_GATT_NOT_CONNECTED; } ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), handle); esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); - if (err != ERR_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char_descr error, err=%d", this->connection_index_, - this->address_str_.c_str(), err); - return err; - } - return ESP_OK; + return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err); } esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) { if (!this->connected()) { - ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT descriptor, not connected.", this->connection_index_, - this->address_str_.c_str()); + this->log_gatt_not_connected_("write", "descriptor"); return ESP_GATT_NOT_CONNECTED; } ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), @@ -224,18 +545,12 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::stri esp_err_t err = esp_ble_gattc_write_char_descr( this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(), response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); - if (err != ERR_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, err=%d", this->connection_index_, - this->address_str_.c_str(), err); - return err; - } - return ESP_OK; + return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err); } esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) { if (!this->connected()) { - ESP_LOGW(TAG, "[%d] [%s] Cannot notify GATT characteristic, not connected.", this->connection_index_, - this->address_str_.c_str()); + this->log_gatt_not_connected_("notify", "characteristic"); return ESP_GATT_NOT_CONNECTED; } @@ -243,29 +558,19 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_, this->address_str_.c_str(), handle); esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle); - if (err != ESP_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_register_for_notify failed, err=%d", this->connection_index_, - this->address_str_.c_str(), err); - return err; - } - } else { - ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_, - this->address_str_.c_str(), handle); - esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle); - if (err != ESP_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_unregister_for_notify failed, err=%d", this->connection_index_, - this->address_str_.c_str(), err); - return err; - } + return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err); } - return ESP_OK; + + ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_, + this->address_str_.c_str(), handle); + esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle); + return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err); } esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() { return this->proxy_->get_advertisement_parser_type(); } -} // namespace bluetooth_proxy -} // namespace esphome +} // namespace esphome::bluetooth_proxy #endif // USE_ESP32 diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 73c034d93b..a975d25d91 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -4,14 +4,14 @@ #include "esphome/components/esp32_ble_client/ble_client_base.h" -namespace esphome { -namespace bluetooth_proxy { +namespace esphome::bluetooth_proxy { 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; @@ -24,22 +24,33 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { esp_err_t notify_characteristic(uint16_t handle, bool enable); + void set_address(uint64_t address) override; + protected: friend class BluetoothProxy; + bool supports_efficient_uuids_() const; + void send_service_for_discovery_(); + void reset_connection_(esp_err_t reason); + void update_allocated_slot_(uint64_t find_value, uint64_t set_value); + void log_connection_error_(const char *operation, esp_gatt_status_t status); + void log_connection_warning_(const char *operation, esp_err_t err); + void log_gatt_not_connected_(const char *action, const char *type); + void log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status); + esp_err_t check_and_log_error_(const char *operation, esp_err_t err); + // Memory optimized layout for 32-bit systems // Group 1: Pointers (4 bytes each, naturally aligned) BluetoothProxy *proxy_; // Group 2: 2-byte types - int16_t send_service_{-2}; // Needs to handle negative values and service count + int16_t send_service_{-3}; // -3 = INIT_SENDING_SERVICES, -2 = DONE_SENDING_SERVICES, >=0 = service index // Group 3: 1-byte types bool seen_mtu_or_services_{false}; // 1 byte used, 1 byte padding }; -} // namespace bluetooth_proxy -} // namespace esphome +} // namespace esphome::bluetooth_proxy #endif // USE_ESP32 diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index a5e8ec0860..723466a5ff 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -3,30 +3,27 @@ #include "esphome/core/log.h" #include "esphome/core/macros.h" #include "esphome/core/application.h" +#include #ifdef USE_ESP32 -namespace esphome { -namespace bluetooth_proxy { +namespace esphome::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])}; -} +// BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE is defined during code generation +// It sets the batch size for BLE advertisements to maximize WiFi efficiency + +// Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response) +static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62, + "BLE advertisement data array size mismatch"); BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } void BluetoothProxy::setup() { + this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS; + this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS; + this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { if (this->api_connection_ != nullptr) { this->send_bluetooth_scanner_state_(state); @@ -39,147 +36,95 @@ 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); } +void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) { + ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(), + connection->address_str().c_str(), espbt::client_state_to_string(state)); +} + +void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) { + ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str().c_str(), + message); +} + +void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) { + ESP_LOGW(TAG, "Cannot %s GATT %s, not connected", action, type); +} + +void BluetoothProxy::handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, + const char *type) { + this->log_not_connected_gatt_(action, type); + this->send_gatt_error(address, handle, ESP_GATT_NOT_CONNECTED); +} + +#ifdef USE_ESP32_BLE_DEVICE bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - 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(); + // Fill in the data directly at current position + auto &adv = advertisements[this->response_.advertisements_len]; adv.address = esp32_ble::ble_addr_to_uint64(result.bda); adv.rssi = result.rssi; adv.address_type = result.ble_addr_type; - adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]); + adv.data_len = length; + std::memcpy(adv.data, result.ble_adv, length); + + this->response_.advertisements_len++; ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); - } - // 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 BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE + if (this->response_.advertisements_len >= BLUETOOTH_PROXY_ADVERTISEMENT_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->response_.advertisements_len == 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); -} + // Send the message + this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); -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(); + ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len); - // 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()); - } - - // 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()); - } - - // 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 the length for the next batch + this->response_.advertisements_len = 0; } void BluetoothProxy::dump_config() { - ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); ESP_LOGCONFIG(TAG, + "Bluetooth Proxy:\n" " Active: %s\n" - " Connections: %d\n" - " Raw advertisements: %s", - YESNO(this->active_), this->connections_.size(), YESNO(this->raw_advertisements_)); -} - -int BluetoothProxy::get_bluetooth_connections_free() { - int free = 0; - for (auto *connection : this->connections_) { - if (connection->address_ == 0) { - free++; - ESP_LOGV(TAG, "[%d] Free connection", connection->get_connection_index()); - } else { - ESP_LOGV(TAG, "[%d] Used connection by [%s]", connection->get_connection_index(), - connection->address_str().c_str()); - } - } - return free; + " Connections: %d", + YESNO(this->active_), this->connection_count_); } void BluetoothProxy::loop() { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { - for (auto *connection : this->connections_) { + for (uint8_t i = 0; i < this->connection_count_; i++) { + auto *connection = this->connections_[i]; if (connection->get_address() != 0 && !connection->disconnect_pending()) { connection->disconnect(); } @@ -188,143 +133,22 @@ 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) { - for (auto *connection : this->connections_) { + for (uint8_t i = 0; i < this->connection_count_; i++) { + auto *connection = this->connections_[i]; if (connection->get_address() == address) return connection; } @@ -332,9 +156,10 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese if (!reserve) return nullptr; - for (auto *connection : this->connections_) { + for (uint8_t i = 0; i < this->connection_count_; i++) { + auto *connection = this->connections_[i]; if (connection->get_address() == 0) { - connection->send_service_ = DONE_SENDING_SERVICES; + connection->send_service_ = INIT_SENDING_SERVICES; connection->set_address(address); // All connections must start at INIT // We only set the state if we allocate the connection @@ -351,8 +176,7 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) { switch (msg.request_type) { case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE: - case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: - case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: { + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: { auto *connection = this->get_connection_(msg.address, true); if (connection == nullptr) { ESP_LOGW(TAG, "No free connections available"); @@ -361,23 +185,10 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest } if (connection->state() == espbt::ClientState::CONNECTED || connection->state() == espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(), - connection->address_str().c_str()); + this->log_connection_request_ignored_(connection, connection->state()); this->send_device_connection(msg.address, true); this->send_connections_free(); return; - } else if (connection->state() == espbt::ClientState::SEARCHING) { - ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device", - connection->get_connection_index(), connection->address_str().c_str()); - return; - } else if (connection->state() == espbt::ClientState::DISCOVERED) { - ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device already discovered", - connection->get_connection_index(), connection->address_str().c_str()); - return; - } else if (connection->state() == espbt::ClientState::READY_TO_CONNECT) { - ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, waiting in line to connect", - connection->get_connection_index(), connection->address_str().c_str()); - return; } else if (connection->state() == espbt::ClientState::CONNECTING) { if (connection->disconnect_pending()) { ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect", @@ -385,29 +196,18 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest connection->cancel_pending_disconnect(); return; } - ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already connecting", connection->get_connection_index(), - connection->address_str().c_str()); - return; - } else if (connection->state() == espbt::ClientState::DISCONNECTING) { - ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device is disconnecting", - connection->get_connection_index(), connection->address_str().c_str()); + this->log_connection_request_ignored_(connection, connection->state()); return; } else if (connection->state() != espbt::ClientState::INIT) { - ESP_LOGW(TAG, "[%d] [%s] Connection already in progress", connection->get_connection_index(), - connection->address_str().c_str()); + this->log_connection_request_ignored_(connection, connection->state()); return; } if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE) { connection->set_connection_type(espbt::ConnectionType::V3_WITH_CACHE); - ESP_LOGI(TAG, "[%d] [%s] Connecting v3 with cache", connection->get_connection_index(), - connection->address_str().c_str()); - } else if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE) { + this->log_connection_info_(connection, "v3 with cache"); + } else { // BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE); - ESP_LOGI(TAG, "[%d] [%s] Connecting v3 without cache", connection->get_connection_index(), - connection->address_str().c_str()); - } else { - connection->set_connection_type(espbt::ConnectionType::V1); - ESP_LOGI(TAG, "[%d] [%s] Connecting v1", connection->get_connection_index(), connection->address_str().c_str()); + this->log_connection_info_(connection, "v3 without cache"); } if (msg.has_address_type) { uint64_to_bd_addr(msg.address, connection->remote_bda_); @@ -465,18 +265,22 @@ 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; } + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: { + ESP_LOGE(TAG, "V1 connections removed"); + this->send_device_connection(msg.address, false); + break; + } } } void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected"); - this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "characteristic"); return; } @@ -489,8 +293,7 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected"); - this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "characteristic"); return; } @@ -503,8 +306,7 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest & void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected"); - this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "descriptor"); return; } @@ -517,8 +319,7 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected"); - this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "descriptor"); return; } @@ -531,8 +332,7 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr || !connection->connected()) { - ESP_LOGW(TAG, "Cannot get GATT services, not connected"); - this->send_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED); + this->handle_gatt_not_connected_(msg.address, 0, "get", "services"); return; } if (!connection->service_count_) { @@ -540,16 +340,14 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer this->send_gatt_services_done(msg.address); return; } - if (connection->send_service_ == - DONE_SENDING_SERVICES) // Only start sending services if we're not already sending them + if (connection->send_service_ == INIT_SENDING_SERVICES) // Start sending services if not started yet connection->send_service_ = 0; } void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected"); - this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->handle_gatt_not_connected_(msg.address, msg.handle, "notify", "characteristic"); return; } @@ -565,7 +363,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 +374,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,20 +385,16 @@ 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) - return; - api::BluetoothConnectionsFreeResponse call; - call.free = this->get_bluetooth_connections_free(); - call.limit = this->get_bluetooth_connections_limit(); - for (auto *connection : this->connections_) { - if (connection->address_ != 0) { - call.allocated.push_back(connection->address_); - } + if (this->api_connection_ != nullptr) { + this->send_connections_free(this->api_connection_); } - this->api_connection_->send_message(call); +} + +void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) { + api_connection->send_message(this->connections_free_response_, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE); } void BluetoothProxy::send_gatt_services_done(uint64_t address) { @@ -610,7 +402,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 +412,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 +421,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 +430,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) { @@ -654,7 +446,6 @@ void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace bluetooth_proxy -} // namespace esphome +} // namespace esphome::bluetooth_proxy #endif // USE_ESP32 diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index f0632350e0..bc8d3ed762 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -2,6 +2,7 @@ #ifdef USE_ESP32 +#include #include #include @@ -18,10 +19,11 @@ #include #include -namespace esphome { -namespace bluetooth_proxy { +namespace esphome::bluetooth_proxy { static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; +static const int DONE_SENDING_SERVICES = -2; +static const int INIT_SENDING_SERVICES = -3; using namespace esp32_ble_client; @@ -49,9 +51,12 @@ enum BluetoothProxySubscriptionFlag : uint32_t { }; class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { + friend class BluetoothConnection; // Allow connection to update connections_free_response_ 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; @@ -60,8 +65,10 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; void register_connection(BluetoothConnection *connection) { - this->connections_.push_back(connection); - connection->proxy_ = this; + if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) { + this->connections_[this->connection_count_++] = connection; + connection->proxy_ = this; + } } void bluetooth_device_request(const api::BluetoothDeviceRequest &msg); @@ -72,15 +79,13 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg); void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg); - int get_bluetooth_connections_free(); - int get_bluetooth_connections_limit() { return this->connections_.size(); } - void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags); void unsubscribe_api_connection(api::APIConnection *api_connection); api::APIConnection *get_api_connection() { return this->api_connection_; } void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); void send_connections_free(); + void send_connections_free(api::APIConnection *api_connection); void send_gatt_services_done(uint64_t address); void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error); void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); @@ -129,27 +134,38 @@ 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); + void log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state); + void log_connection_info_(BluetoothConnection *connection, const char *message); + void log_not_connected_gatt_(const char *action, const char *type); + void handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, const char *type); // Memory optimized layout for 32-bit systems // Group 1: Pointers (4 bytes each, naturally aligned) api::APIConnection *api_connection_{nullptr}; - // Group 2: Container types (typically 12 bytes on 32-bit) - std::vector connections_{}; + // Group 2: Fixed-size array of connection pointers + std::array connections_{}; - // Group 3: 1-byte types grouped together + // BLE advertisement batching + api::BluetoothLERawAdvertisementsResponse response_; + + // Group 3: 4-byte types + uint32_t last_advertisement_flush_time_{0}; + + // Pre-allocated response message - always ready to send + api::BluetoothConnectionsFreeResponse connections_free_response_; + + // Group 4: 1-byte types grouped together bool active_; - bool raw_advertisements_{false}; + uint8_t connection_count_{0}; // 2 bytes used, 2 bytes padding }; extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace bluetooth_proxy -} // namespace esphome +} // namespace esphome::bluetooth_proxy #endif // USE_ESP32 diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index d2524e5aac..86b65d361d 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -7,6 +7,8 @@ #include #include +#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID" + namespace esphome { namespace bme280_base { @@ -88,7 +90,6 @@ const char *oversampling_to_str(BME280Oversampling oversampling) { // NOLINT } void BME280Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t chip_id = 0; // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries @@ -99,18 +100,18 @@ void BME280Component::setup() { if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); return; } if (chip_id != 0x60) { this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(); + this->mark_failed(BME280_ERROR_WRONG_CHIP_ID); return; } // Send a soft reset. if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) { - this->mark_failed(); + this->mark_failed("Reset failed"); return; } // Wait until the NVM data has finished loading. @@ -119,14 +120,12 @@ void BME280Component::setup() { do { // NOLINT delay(2); if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { - ESP_LOGW(TAG, "Error reading status register."); - this->mark_failed(); + this->mark_failed("Error reading status register"); return; } } while ((status & BME280_STATUS_IM_UPDATE) && (--retry)); if (status & BME280_STATUS_IM_UPDATE) { - ESP_LOGW(TAG, "Timeout loading NVM."); - this->mark_failed(); + this->mark_failed("Timeout loading NVM"); return; } @@ -154,26 +153,26 @@ void BME280Component::setup() { uint8_t humid_control_val = 0; if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) { - this->mark_failed(); + this->mark_failed("Read humidity control"); return; } humid_control_val &= ~0b00000111; humid_control_val |= this->humidity_oversampling_ & 0b111; if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) { - this->mark_failed(); + this->mark_failed("Write humidity control"); return; } uint8_t config_register = 0; if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) { - this->mark_failed(); + this->mark_failed("Read config"); return; } config_register &= ~0b11111100; config_register |= 0b101 << 5; // 1000 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) { - this->mark_failed(); + this->mark_failed("Write config"); return; } } @@ -184,7 +183,7 @@ void BME280Component::dump_config() { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); break; case WRONG_CHIP_ID: - ESP_LOGE(TAG, "BME280 has wrong chip ID! Is it a BME280?"); + ESP_LOGE(TAG, BME280_ERROR_WRONG_CHIP_ID); break; case NONE: default: @@ -224,21 +223,21 @@ void BME280Component::update() { this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { uint8_t data[8]; if (!this->read_bytes(BME280_REGISTER_MEASUREMENTS, data, 8)) { - ESP_LOGW(TAG, "Error reading registers."); + ESP_LOGW(TAG, "Error reading registers"); this->status_set_warning(); return; } int32_t t_fine = 0; float const temperature = this->read_temperature_(data, &t_fine); if (std::isnan(temperature)) { - ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values."); + ESP_LOGW(TAG, "Invalid temperature"); this->status_set_warning(); return; } float const pressure = this->read_pressure_(data, t_fine); float const humidity = this->read_humidity_(data, t_fine); - ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity); + ESP_LOGV(TAG, "Temperature=%.1f°C Pressure=%.1fhPa Humidity=%.1f%%", temperature, pressure, humidity); if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); if (this->pressure_sensor_ != nullptr) diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 7e8f2f5a32..16435ccfee 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -28,7 +28,7 @@ const float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0, const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; -static const char *oversampling_to_str(BME680Oversampling oversampling) { +[[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) { switch (oversampling) { case BME680_OVERSAMPLING_NONE: return "None"; @@ -47,7 +47,7 @@ static const char *oversampling_to_str(BME680Oversampling oversampling) { } } -static const char *iir_filter_to_str(BME680IIRFilter filter) { +[[maybe_unused]] static const char *iir_filter_to_str(BME680IIRFilter filter) { switch (filter) { case BME680_IIR_FILTER_OFF: return "OFF"; @@ -71,7 +71,6 @@ static const char *iir_filter_to_str(BME680IIRFilter filter) { } void BME680Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t chip_id; if (!this->read_byte(BME680_REGISTER_CHIPID, &chip_id) || chip_id != 0x61) { this->mark_failed(); diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 562d39e7b5..d969c8fd98 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -15,8 +15,6 @@ std::vector uint8_t BME680BSECComponent::work_buffer_[BSEC_MAX_WORKBUFFER_SIZE] = {0}; void BME680BSECComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->device_id_.c_str()); - uint8_t new_idx = BME680BSECComponent::instances.size(); BME680BSECComponent::instances.push_back(this); diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index a23711c4ca..f5dcfd65a1 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -21,8 +21,6 @@ static const char *const TAG = "bme68x_bsec2.sensor"; static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; void BME68xBSEC2Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->bsec_status_ = bsec_init_m(&this->bsec_instance_); if (this->bsec_status_ != BSEC_OK) { this->mark_failed(); diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp index aca42f1b52..b041c7c2dc 100644 --- a/esphome/components/bmi160/bmi160.cpp +++ b/esphome/components/bmi160/bmi160.cpp @@ -119,7 +119,6 @@ const float GRAVITY_EARTH = 9.80665f; void BMI160Component::internal_setup_(int stage) { switch (stage) { case 0: - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t chipid; if (!this->read_byte(BMI160_REGISTER_CHIPID, &chipid) || (chipid != 0b11010001)) { this->mark_failed(); diff --git a/esphome/components/bmp085/bmp085.cpp b/esphome/components/bmp085/bmp085.cpp index 94dc61891b..657da34f9b 100644 --- a/esphome/components/bmp085/bmp085.cpp +++ b/esphome/components/bmp085/bmp085.cpp @@ -20,7 +20,6 @@ void BMP085Component::update() { this->set_timeout("temperature", 5, [this]() { this->read_temperature_(); }); } void BMP085Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t data[22]; if (!this->read_bytes(BMP085_REGISTER_AC1_H, data, 22)) { this->mark_failed(); diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 94b8bd6540..e3cc2d9a57 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -2,6 +2,8 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" +#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID" + namespace esphome { namespace bmp280_base { @@ -57,30 +59,29 @@ static const char *iir_filter_to_str(BMP280IIRFilter filter) { } void BMP280Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t chip_id = 0; // Read the chip id twice, to work around a bug where the first read is 0. // https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855 if (!this->read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); return; } if (!this->read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); return; } if (chip_id != 0x58) { this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(); + this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID); return; } // Send a soft reset. if (!this->write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { - this->mark_failed(); + this->mark_failed("Reset failed"); return; } // Wait until the NVM data has finished loading. @@ -89,14 +90,12 @@ void BMP280Component::setup() { do { delay(2); if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) { - ESP_LOGW(TAG, "Error reading status register."); - this->mark_failed(); + this->mark_failed("Error reading status register"); return; } } while ((status & BMP280_STATUS_IM_UPDATE) && (--retry)); if (status & BMP280_STATUS_IM_UPDATE) { - ESP_LOGW(TAG, "Timeout loading NVM."); - this->mark_failed(); + this->mark_failed("Timeout loading NVM"); return; } @@ -117,14 +116,14 @@ void BMP280Component::setup() { uint8_t config_register = 0; if (!this->read_byte(BMP280_REGISTER_CONFIG, &config_register)) { - this->mark_failed(); + this->mark_failed("Read config"); return; } config_register &= ~0b11111100; config_register |= 0b000 << 5; // 0.5 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; if (!this->write_byte(BMP280_REGISTER_CONFIG, config_register)) { - this->mark_failed(); + this->mark_failed("Write config"); return; } } @@ -135,7 +134,7 @@ void BMP280Component::dump_config() { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); break; case WRONG_CHIP_ID: - ESP_LOGE(TAG, "BMP280 has wrong chip ID! Is it a BME280?"); + ESP_LOGE(TAG, BMP280_ERROR_WRONG_CHIP_ID); break; case NONE: default: @@ -173,13 +172,13 @@ void BMP280Component::update() { int32_t t_fine = 0; float temperature = this->read_temperature_(&t_fine); if (std::isnan(temperature)) { - ESP_LOGW(TAG, "Invalid temperature, cannot read pressure values."); + ESP_LOGW(TAG, "Invalid temperature"); this->status_set_warning(); return; } float pressure = this->read_pressure_(t_fine); - ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa", temperature, pressure); + ESP_LOGV(TAG, "Temperature=%.1f°C Pressure=%.1fhPa", temperature, pressure); if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); if (this->pressure_sensor_ != nullptr) diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.cpp b/esphome/components/bmp3xx_base/bmp3xx_base.cpp index 979f354cb2..acc28d4e85 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.cpp +++ b/esphome/components/bmp3xx_base/bmp3xx_base.cpp @@ -70,7 +70,6 @@ static const LogString *iir_filter_to_str(IIRFilter filter) { void BMP3XXComponent::setup() { this->error_code_ = NONE; - ESP_LOGCONFIG(TAG, "Running setup"); // Call the Device base class "initialise" function if (!reset()) { ESP_LOGE(TAG, "Failed to reset"); diff --git a/esphome/components/bmp581/bmp581.cpp b/esphome/components/bmp581/bmp581.cpp index 2204a6af2e..301fc31df0 100644 --- a/esphome/components/bmp581/bmp581.cpp +++ b/esphome/components/bmp581/bmp581.cpp @@ -128,8 +128,6 @@ void BMP581Component::setup() { */ this->error_code_ = NONE; - ESP_LOGCONFIG(TAG, "Running setup"); - //////////////////// // 1) Soft reboot // //////////////////// diff --git a/esphome/components/bp1658cj/bp1658cj.cpp b/esphome/components/bp1658cj/bp1658cj.cpp index b502a738cd..b8ad5dc3d2 100644 --- a/esphome/components/bp1658cj/bp1658cj.cpp +++ b/esphome/components/bp1658cj/bp1658cj.cpp @@ -15,7 +15,6 @@ static const uint8_t BP1658CJ_ADDR_START_5CH = 0x30; static const uint8_t BP1658CJ_DELAY = 2; void BP1658CJ::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->data_pin_->setup(); this->data_pin_->digital_write(false); this->clock_pin_->setup(); diff --git a/esphome/components/bp5758d/bp5758d.cpp b/esphome/components/bp5758d/bp5758d.cpp index 797ddd919e..4f330b9c77 100644 --- a/esphome/components/bp5758d/bp5758d.cpp +++ b/esphome/components/bp5758d/bp5758d.cpp @@ -20,7 +20,6 @@ static const uint8_t BP5758D_ALL_DATA_CHANNEL_ENABLEMENT = 0b00011111; static const uint8_t BP5758D_DELAY = 2; void BP5758D::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->data_pin_->setup(); this->data_pin_->digital_write(false); delayMicroseconds(BP5758D_DELAY); diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index ed2670a5c5..a23958989e 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -137,4 +137,3 @@ async def button_press_to_code(config, action_id, template_arg, args): @coroutine_with_priority(100.0) async def to_code(config): cg.add_global(button_ns.using) - cg.add_define("USE_BUTTON") diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index cdb57fd481..e1de1eb2f2 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -22,9 +22,8 @@ def validate_id(config): if CONF_CAN_ID in config: can_id = config[CONF_CAN_ID] id_ext = config[CONF_USE_EXTENDED_ID] - if not id_ext: - if can_id > 0x7FF: - raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") + if not id_ext and can_id > 0x7FF: + raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") return config diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index d08558037e..6e61f05be7 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -7,7 +7,6 @@ namespace canbus { static const char *const TAG = "canbus"; void Canbus::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (!this->setup_internal()) { ESP_LOGE(TAG, "setup error!"); this->mark_failed(); diff --git a/esphome/components/cap1188/cap1188.cpp b/esphome/components/cap1188/cap1188.cpp index af167deb99..584ff896c5 100644 --- a/esphome/components/cap1188/cap1188.cpp +++ b/esphome/components/cap1188/cap1188.cpp @@ -8,8 +8,6 @@ namespace cap1188 { static const char *const TAG = "cap1188"; void CAP1188Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Reset device using the reset pin if (this->reset_pin_ != nullptr) { this->reset_pin_->setup(); diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 7e8afd8fab..cd69b67c78 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -14,7 +14,7 @@ from esphome.core import CORE, coroutine_with_priority AUTO_LOAD = ["web_server_base", "ota.web_server"] DEPENDENCIES = ["wifi"] -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] captive_portal_ns = cg.esphome_ns.namespace("captive_portal") CaptivePortal = captive_portal_ns.class_("CaptivePortal", cg.Component) diff --git a/esphome/components/cd74hc4067/cd74hc4067.cpp b/esphome/components/cd74hc4067/cd74hc4067.cpp index 3c7b9038d7..174dc676f9 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.cpp +++ b/esphome/components/cd74hc4067/cd74hc4067.cpp @@ -10,8 +10,6 @@ static const char *const TAG = "cd74hc4067"; float CD74HC4067Component::get_setup_priority() const { return setup_priority::DATA; } void CD74HC4067Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->pin_s0_->setup(); this->pin_s1_->setup(); this->pin_s2_->setup(); diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index 325c56e470..6f652cb0c6 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -14,7 +14,6 @@ static const uint8_t CH422G_REG_OUT_UPPER = 0x23; // write reg for output bit static const char *const TAG = "ch422g"; void CH422GComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // set outputs before mode this->write_outputs_(); // Set mode and check for errors diff --git a/esphome/components/chsc6x/chsc6x_touchscreen.cpp b/esphome/components/chsc6x/chsc6x_touchscreen.cpp index 524fa1eb36..31c9466691 100644 --- a/esphome/components/chsc6x/chsc6x_touchscreen.cpp +++ b/esphome/components/chsc6x/chsc6x_touchscreen.cpp @@ -4,7 +4,6 @@ namespace esphome { namespace chsc6x { void CHSC6XTouchscreen::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (this->interrupt_pin_ != nullptr) { this->interrupt_pin_->setup(); this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); @@ -15,8 +14,6 @@ void CHSC6XTouchscreen::setup() { if (this->y_raw_max_ == this->y_raw_min_) { this->y_raw_max_ = this->display_->get_native_height(); } - - ESP_LOGCONFIG(TAG, "CHSC6X Touchscreen setup complete"); } void CHSC6XTouchscreen::update_touches() { diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 9530ecdcca..4af3a619b5 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -519,5 +519,4 @@ async def climate_control_to_code(config, action_id, template_arg, args): @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_CLIMATE") cg.add_global(climate_ns.using) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index c3a0dfca8f..8bd4714753 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -5,6 +5,13 @@ #include namespace esphome { + +#ifdef USE_API +namespace api { +class APIConnection; +} // namespace api +#endif + namespace climate { /** This class contains all static data for climate devices. @@ -173,6 +180,23 @@ class ClimateTraits { void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } protected: +#ifdef USE_API + // The API connection is a friend class to access internal methods + friend class api::APIConnection; + // These methods return references to internal data structures. + // They are used by the API to avoid copying data when encoding messages. + // Warning: Do not use these methods outside of the API connection code. + // They return references to internal data that can be invalidated. + const std::set &get_supported_modes_for_api_() const { return this->supported_modes_; } + const std::set &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; } + const std::set &get_supported_custom_fan_modes_for_api_() const { + return this->supported_custom_fan_modes_; + } + const std::set &get_supported_presets_for_api_() const { return this->supported_presets_; } + const std::set &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; } + const std::set &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; } +#endif + void set_mode_support_(climate::ClimateMode mode, bool supported) { if (supported) { this->supported_modes_.insert(mode); diff --git a/esphome/components/cm1106/cm1106.cpp b/esphome/components/cm1106/cm1106.cpp index 109524c04a..339a1659ac 100644 --- a/esphome/components/cm1106/cm1106.cpp +++ b/esphome/components/cm1106/cm1106.cpp @@ -20,7 +20,6 @@ uint8_t cm1106_checksum(const uint8_t *response, size_t len) { } void CM1106Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t response[8] = {0}; if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index b084622f4c..19924f0da7 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -3,6 +3,12 @@ CODEOWNERS = ["@esphome/core"] CONF_BYTE_ORDER = "byte_order" +BYTE_ORDER_LITTLE = "little_endian" +BYTE_ORDER_BIG = "big_endian" + +CONF_COLOR_DEPTH = "color_depth" CONF_DRAW_ROUNDING = "draw_rounding" +CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" +CONF_USE_PSRAM = "use_psram" diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index cd97a38ecc..0e01eb336f 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -265,5 +265,4 @@ async def cover_control_to_code(config, action_id, template_arg, args): @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_COVER") cg.add_global(cover_ns.using) diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index d139bab8ee..68dfab111b 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -99,43 +99,39 @@ const optional &CoverCall::get_tilt() const { return this->tilt_; } const optional &CoverCall::get_toggle() const { return this->toggle_; } void CoverCall::validate_() { auto traits = this->parent_->get_traits(); + const char *name = this->parent_->get_name().c_str(); + if (this->position_.has_value()) { auto pos = *this->position_; if (!traits.get_supports_position() && pos != COVER_OPEN && pos != COVER_CLOSED) { - ESP_LOGW(TAG, "'%s' - This cover device does not support setting position!", this->parent_->get_name().c_str()); + ESP_LOGW(TAG, "'%s': position unsupported", name); this->position_.reset(); } else if (pos < 0.0f || pos > 1.0f) { - ESP_LOGW(TAG, "'%s' - Position %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), pos); + ESP_LOGW(TAG, "'%s': position %.2f out of range", name, pos); this->position_ = clamp(pos, 0.0f, 1.0f); } } if (this->tilt_.has_value()) { auto tilt = *this->tilt_; if (!traits.get_supports_tilt()) { - ESP_LOGW(TAG, "'%s' - This cover device does not support tilt!", this->parent_->get_name().c_str()); + ESP_LOGW(TAG, "'%s': tilt unsupported", name); this->tilt_.reset(); } else if (tilt < 0.0f || tilt > 1.0f) { - ESP_LOGW(TAG, "'%s' - Tilt %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), tilt); + ESP_LOGW(TAG, "'%s': tilt %.2f out of range", name, tilt); this->tilt_ = clamp(tilt, 0.0f, 1.0f); } } if (this->toggle_.has_value()) { if (!traits.get_supports_toggle()) { - ESP_LOGW(TAG, "'%s' - This cover device does not support toggle!", this->parent_->get_name().c_str()); + ESP_LOGW(TAG, "'%s': toggle unsupported", name); this->toggle_.reset(); } } if (this->stop_) { - if (this->position_.has_value()) { - ESP_LOGW(TAG, "Cannot set position when stopping a cover!"); + if (this->position_.has_value() || this->tilt_.has_value() || this->toggle_.has_value()) { + ESP_LOGW(TAG, "'%s': cannot position/tilt/toggle when stopping", name); this->position_.reset(); - } - if (this->tilt_.has_value()) { - ESP_LOGW(TAG, "Cannot set tilt when stopping a cover!"); this->tilt_.reset(); - } - if (this->toggle_.has_value()) { - ESP_LOGW(TAG, "Cannot set toggle when stopping a cover!"); this->toggle_.reset(); } } diff --git a/esphome/components/cs5460a/cs5460a.cpp b/esphome/components/cs5460a/cs5460a.cpp index e3a5941d94..e026eccf80 100644 --- a/esphome/components/cs5460a/cs5460a.cpp +++ b/esphome/components/cs5460a/cs5460a.cpp @@ -52,8 +52,6 @@ bool CS5460AComponent::softreset_() { } void CS5460AComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - float current_full_scale = (pga_gain_ == CS5460A_PGA_GAIN_10X) ? 0.25 : 0.10; float voltage_full_scale = 0.25; current_multiplier_ = current_full_scale / (fabsf(current_gain_) * 0x1000000); diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 6c3d457f26..482636dd81 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -42,7 +42,6 @@ static const uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation enum CSE7761 { RMS_IAC, RMS_IBC, RMS_UC, POWER_PAC, POWER_PBC, POWER_SC, ENERGY_AC, ENERGY_BC }; void CSE7761Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_RESET); uint16_t syscon = this->read_(0x00, 2); // Default 0x0A04 if ((0x0A04 == syscon) && this->chip_init_()) { diff --git a/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp b/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp index c444dd7485..e65997b7fc 100644 --- a/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp +++ b/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp @@ -6,7 +6,6 @@ namespace cst226 { static const char *const TAG = "cst226.touchscreen"; void CST226Touchscreen::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (this->reset_pin_ != nullptr) { this->reset_pin_->setup(); this->reset_pin_->digital_write(true); @@ -95,7 +94,6 @@ void CST226Touchscreen::continue_setup_() { } } this->setup_complete_ = true; - ESP_LOGCONFIG(TAG, "CST226 Touchscreen setup complete"); } void CST226Touchscreen::update_button_state_(bool state) { if (this->button_touched_ == state) diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 0c5099d4f0..0ba2d9df94 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -35,11 +35,9 @@ void CST816Touchscreen::continue_setup_() { if (this->y_raw_max_ == this->y_raw_min_) { this->y_raw_max_ = this->display_->get_native_height(); } - ESP_LOGCONFIG(TAG, "CST816 Touchscreen setup complete"); } void CST816Touchscreen::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (this->reset_pin_ != nullptr) { this->reset_pin_->setup(); this->reset_pin_->digital_write(true); diff --git a/esphome/components/dac7678/dac7678_output.cpp b/esphome/components/dac7678/dac7678_output.cpp index 5c10bbc1bc..83f8722e7f 100644 --- a/esphome/components/dac7678/dac7678_output.cpp +++ b/esphome/components/dac7678/dac7678_output.cpp @@ -20,8 +20,6 @@ static const uint8_t DAC7678_REG_INTERNAL_REF_0 = 0x80; static const uint8_t DAC7678_REG_INTERNAL_REF_1 = 0x90; void DAC7678Output::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - ESP_LOGV(TAG, "Resetting device"); // Reset device diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index 3796a888fd..5cd6063893 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -70,7 +70,6 @@ bool DallasTemperatureSensor::read_scratch_pad_() { } void DallasTemperatureSensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (!this->check_address_()) return; if (!this->read_scratch_pad_()) diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 4788810965..1d84b75f26 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -164,7 +164,6 @@ async def register_datetime(var, config): cg.add(getattr(cg.App, f"register_{entity_type}")(var)) CORE.register_platform_component(entity_type, var) await setup_datetime_core_(var, config) - cg.add_define(f"USE_DATETIME_{config[CONF_TYPE]}") async def new_datetime(config, *args): @@ -175,7 +174,6 @@ async def new_datetime(config, *args): @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_DATETIME") cg.add_global(datetime_ns.using) diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index 500dfac1fe..6b4205a545 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.components.zephyr import zephyr_add_prj_conf from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -10,8 +11,9 @@ from esphome.const import ( CONF_LOOP_TIME, PlatformFramework, ) +from esphome.core import CORE -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["logger"] CONF_DEBUG_ID = "debug_id" @@ -44,6 +46,17 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + if CORE.using_zephyr: + zephyr_add_prj_conf("HWINFO", True) + # gdb thread support + zephyr_add_prj_conf("DEBUG_THREAD_INFO", True) + # RTT + zephyr_add_prj_conf("USE_SEGGER_RTT", True) + zephyr_add_prj_conf("RTT_CONSOLE", True) + zephyr_add_prj_conf("LOG", True) + zephyr_add_prj_conf("LOG_BLOCK_IN_THREAD", True) + zephyr_add_prj_conf("LOG_BUFFER_SIZE", 4096) + zephyr_add_prj_conf("SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL", True) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -62,5 +75,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, + "debug_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp new file mode 100644 index 0000000000..9a361b158f --- /dev/null +++ b/esphome/components/debug/debug_zephyr.cpp @@ -0,0 +1,281 @@ +#include "debug_component.h" +#ifdef USE_ZEPHYR +#include +#include "esphome/core/log.h" +#include +#include +#include + +#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0] + +namespace esphome { +namespace debug { + +static const char *const TAG = "debug"; +constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; +constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8; + +static void show_reset_reason(std::string &reset_reason, bool set, const char *reason) { + if (!set) { + return; + } + if (!reset_reason.empty()) { + reset_reason += ", "; + } + reset_reason += reason; +} + +inline uint32_t read_mem_u32(uintptr_t addr) { + return *reinterpret_cast(addr); // NOLINT(performance-no-int-to-ptr) +} + +std::string DebugComponent::get_reset_reason_() { + uint32_t cause; + auto ret = hwinfo_get_reset_cause(&cause); + if (ret) { + ESP_LOGE(TAG, "Unable to get reset cause: %d", ret); + return ""; + } + std::string reset_reason; + + show_reset_reason(reset_reason, cause & RESET_PIN, "External pin"); + show_reset_reason(reset_reason, cause & RESET_SOFTWARE, "Software reset"); + show_reset_reason(reset_reason, cause & RESET_BROWNOUT, "Brownout (drop in voltage)"); + show_reset_reason(reset_reason, cause & RESET_POR, "Power-on reset (POR)"); + show_reset_reason(reset_reason, cause & RESET_WATCHDOG, "Watchdog timer expiration"); + show_reset_reason(reset_reason, cause & RESET_DEBUG, "Debug event"); + show_reset_reason(reset_reason, cause & RESET_SECURITY, "Security violation"); + show_reset_reason(reset_reason, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode"); + show_reset_reason(reset_reason, cause & RESET_CPU_LOCKUP, "CPU lock-up detected"); + show_reset_reason(reset_reason, cause & RESET_PARITY, "Parity error"); + show_reset_reason(reset_reason, cause & RESET_PLL, "PLL error"); + show_reset_reason(reset_reason, cause & RESET_CLOCK, "Clock error"); + show_reset_reason(reset_reason, cause & RESET_HARDWARE, "Hardware reset"); + show_reset_reason(reset_reason, cause & RESET_USER, "User reset"); + show_reset_reason(reset_reason, cause & RESET_TEMPERATURE, "Temperature reset"); + + ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str()); + return reset_reason; +} + +uint32_t DebugComponent::get_free_heap_() { return INT_MAX; } + +void DebugComponent::get_device_info_(std::string &device_info) { + std::string supply = "Main supply status: "; + if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) { + supply += "Normal voltage."; + } else { + supply += "High voltage."; + } + ESP_LOGD(TAG, "%s", supply.c_str()); + device_info += "|" + supply; + + std::string reg0 = "Regulator stage 0: "; + if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) { + reg0 += nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO"; + reg0 += ", "; + switch (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) { + case (UICR_REGOUT0_VOUT_DEFAULT << UICR_REGOUT0_VOUT_Pos): + reg0 += "1.8V (default)"; + break; + case (UICR_REGOUT0_VOUT_1V8 << UICR_REGOUT0_VOUT_Pos): + reg0 += "1.8V"; + break; + case (UICR_REGOUT0_VOUT_2V1 << UICR_REGOUT0_VOUT_Pos): + reg0 += "2.1V"; + break; + case (UICR_REGOUT0_VOUT_2V4 << UICR_REGOUT0_VOUT_Pos): + reg0 += "2.4V"; + break; + case (UICR_REGOUT0_VOUT_2V7 << UICR_REGOUT0_VOUT_Pos): + reg0 += "2.7V"; + break; + case (UICR_REGOUT0_VOUT_3V0 << UICR_REGOUT0_VOUT_Pos): + reg0 += "3.0V"; + break; + case (UICR_REGOUT0_VOUT_3V3 << UICR_REGOUT0_VOUT_Pos): + reg0 += "3.3V"; + break; + default: + reg0 += "???V"; + } + } else { + reg0 += "disabled"; + } + ESP_LOGD(TAG, "%s", reg0.c_str()); + device_info += "|" + reg0; + + std::string reg1 = "Regulator stage 1: "; + reg1 += nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO"; + ESP_LOGD(TAG, "%s", reg1.c_str()); + device_info += "|" + reg1; + + std::string usb_power = "USB power state: "; + if (nrf_power_usbregstatus_vbusdet_get(NRF_POWER)) { + if (nrf_power_usbregstatus_outrdy_get(NRF_POWER)) { + /**< From the power viewpoint, USB is ready for working. */ + usb_power += "ready"; + } else { + /**< The USB power is detected, but USB power regulator is not ready. */ + usb_power += "connected (regulator is not ready)"; + } + } else { + /**< No power on USB lines detected. */ + usb_power += "disconected"; + } + ESP_LOGD(TAG, "%s", usb_power.c_str()); + device_info += "|" + usb_power; + + bool enabled; + nrf_power_pof_thr_t pof_thr; + + pof_thr = nrf_power_pofcon_get(NRF_POWER, &enabled); + std::string pof = "Power-fail comparator: "; + if (enabled) { + switch (pof_thr) { + case POWER_POFCON_THRESHOLD_V17: + pof += "1.7V"; + break; + case POWER_POFCON_THRESHOLD_V18: + pof += "1.8V"; + break; + case POWER_POFCON_THRESHOLD_V19: + pof += "1.9V"; + break; + case POWER_POFCON_THRESHOLD_V20: + pof += "2.0V"; + break; + case POWER_POFCON_THRESHOLD_V21: + pof += "2.1V"; + break; + case POWER_POFCON_THRESHOLD_V22: + pof += "2.2V"; + break; + case POWER_POFCON_THRESHOLD_V23: + pof += "2.3V"; + break; + case POWER_POFCON_THRESHOLD_V24: + pof += "2.4V"; + break; + case POWER_POFCON_THRESHOLD_V25: + pof += "2.5V"; + break; + case POWER_POFCON_THRESHOLD_V26: + pof += "2.6V"; + break; + case POWER_POFCON_THRESHOLD_V27: + pof += "2.7V"; + break; + case POWER_POFCON_THRESHOLD_V28: + pof += "2.8V"; + break; + } + + if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) { + pof += ", VDDH: "; + switch (nrf_power_pofcon_vddh_get(NRF_POWER)) { + case NRF_POWER_POFTHRVDDH_V27: + pof += "2.7V"; + break; + case NRF_POWER_POFTHRVDDH_V28: + pof += "2.8V"; + break; + case NRF_POWER_POFTHRVDDH_V29: + pof += "2.9V"; + break; + case NRF_POWER_POFTHRVDDH_V30: + pof += "3.0V"; + break; + case NRF_POWER_POFTHRVDDH_V31: + pof += "3.1V"; + break; + case NRF_POWER_POFTHRVDDH_V32: + pof += "3.2V"; + break; + case NRF_POWER_POFTHRVDDH_V33: + pof += "3.3V"; + break; + case NRF_POWER_POFTHRVDDH_V34: + pof += "3.4V"; + break; + case NRF_POWER_POFTHRVDDH_V35: + pof += "3.5V"; + break; + case NRF_POWER_POFTHRVDDH_V36: + pof += "3.6V"; + break; + case NRF_POWER_POFTHRVDDH_V37: + pof += "3.7V"; + break; + case NRF_POWER_POFTHRVDDH_V38: + pof += "3.8V"; + break; + case NRF_POWER_POFTHRVDDH_V39: + pof += "3.9V"; + break; + case NRF_POWER_POFTHRVDDH_V40: + pof += "4.0V"; + break; + case NRF_POWER_POFTHRVDDH_V41: + pof += "4.1V"; + break; + case NRF_POWER_POFTHRVDDH_V42: + pof += "4.2V"; + break; + } + } + } else { + pof += "disabled"; + } + ESP_LOGD(TAG, "%s", pof.c_str()); + device_info += "|" + pof; + + auto package = [](uint32_t value) { + switch (value) { + case 0x2004: + return "QIxx - 7x7 73-pin aQFN"; + case 0x2000: + return "QFxx - 6x6 48-pin QFN"; + case 0x2005: + return "CKxx - 3.544 x 3.607 WLCSP"; + } + return "Unspecified"; + }; + + ESP_LOGD(TAG, "Code page size: %u, code size: %u, device id: 0x%08x%08x", NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, + NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0]); + ESP_LOGD(TAG, "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x", NRF_FICR->ER[0], + NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2], + NRF_FICR->IR[3]); + ESP_LOGD(TAG, "Device address type: %s, address: %s", (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), + get_mac_address_pretty().c_str()); + ESP_LOGD(TAG, "Part code: nRF%x, version: %c%c%c%c, package: %s", NRF_FICR->INFO.PART, + NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF, NRF_FICR->INFO.VARIANT >> 8 & 0xFF, + NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE)); + ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, + (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); + ESP_LOGD( + TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", + YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), + YESNO(((NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) != + (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)) || + ((NRF_UICR->PSELRESET[1] & UICR_PSELRESET_CONNECT_Msk) != + (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)))); + +#ifdef USE_BOOTLOADER_MCUBOOT + ESP_LOGD(TAG, "bootloader: mcuboot"); +#else + ESP_LOGD(TAG, "bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, + (BOOTLOADER_VERSION_REGISTER >> 8) & 0xFF, BOOTLOADER_VERSION_REGISTER & 0xFF); + ESP_LOGD(TAG, "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x", read_mem_u32(MBR_BOOTLOADER_ADDR), + NRF_UICR->NRFFW[0]); + ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR), + NRF_UICR->NRFFW[1]); +#endif +} + +void DebugComponent::update_platform_() {} + +} // namespace debug +} // namespace esphome +#endif diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index 4669095d5d..4484f15935 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import sensor from esphome.components.esp32 import CONF_CPU_FREQUENCY +from esphome.components.psram import DOMAIN as PSRAM_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_BLOCK, @@ -54,7 +55,7 @@ CONFIG_SCHEMA = { ), cv.Optional(CONF_PSRAM): cv.All( cv.only_on_esp32, - cv.requires_component("psram"), + cv.requires_component(PSRAM_DOMAIN), sensor.sensor_schema( unit_of_measurement=UNIT_BYTES, icon=ICON_COUNTER, diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 84fc102b66..8066b411ff 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -12,7 +12,6 @@ static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000; bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void DeepSleepComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); global_has_deep_sleep = true; const optional run_duration = get_run_duration_(); diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index 7965ab738a..c5c1fe0835 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -1,4 +1,5 @@ #ifdef USE_ESP32 +#include "driver/gpio.h" #include "deep_sleep_component.h" #include "esphome/core/log.h" @@ -74,11 +75,20 @@ void DeepSleepComponent::deep_sleep_() { if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); if (this->wakeup_pin_ != nullptr) { + const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin()); + if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) { + gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY); + } else if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLDOWN) { + gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY); + } + gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT); + gpio_hold_en(gpio_pin); + gpio_deep_sleep_hold_en(); bool level = !this->wakeup_pin_->is_inverted(); if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { level = !level; } - esp_sleep_enable_ext0_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); + esp_sleep_enable_ext0_wakeup(gpio_pin, level); } if (this->ext1_wakeup_.has_value()) { esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode); @@ -102,6 +112,15 @@ void DeepSleepComponent::deep_sleep_() { if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); if (this->wakeup_pin_ != nullptr) { + const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin()); + if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLUP) { + gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY); + } else if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLDOWN) { + gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY); + } + gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT); + gpio_hold_en(gpio_pin); + gpio_deep_sleep_hold_en(); bool level = !this->wakeup_pin_->is_inverted(); if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { level = !level; diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index 8c9438dccb..d54b147036 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -74,8 +74,7 @@ def range_segment_list(input): if isinstance(input, list): for list_item in input: if isinstance(list_item, list): - for item in list_item: - flat_list.append(item) + flat_list.extend(list_item) else: flat_list.append(list_item) else: diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 7248ef624e..cc0bf55a80 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -8,7 +8,6 @@ namespace dht { static const char *const TAG = "dht"; void DHT::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->pin_->digital_write(true); this->pin_->setup(); this->pin_->digital_write(true); diff --git a/esphome/components/dht12/dht12.cpp b/esphome/components/dht12/dht12.cpp index 54a6688b0b..445d150be0 100644 --- a/esphome/components/dht12/dht12.cpp +++ b/esphome/components/dht12/dht12.cpp @@ -34,7 +34,6 @@ void DHT12Component::update() { this->status_clear_warning(); } void DHT12Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t data[5]; if (!this->read_data_(data)) { this->mark_failed(); diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 12c63231e7..8021a8f9b1 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -12,6 +12,8 @@ from esphome.const import ( CONF_ROTATION, CONF_TO, CONF_TRIGGER_ID, + CONF_UPDATE_INTERVAL, + SCHEDULER_DONT_RUN, ) from esphome.core import coroutine_with_priority @@ -51,8 +53,7 @@ DISPLAY_ROTATIONS = { def validate_rotation(value): value = cv.string(value) - if value.endswith("°"): - value = value[:-1] + value = value.removesuffix("°") return cv.enum(DISPLAY_ROTATIONS, int=True)(value) @@ -68,6 +69,18 @@ BASIC_DISPLAY_SCHEMA = cv.Schema( } ).extend(cv.polling_component_schema("1s")) + +def _validate_test_card(config): + if ( + config.get(CONF_SHOW_TEST_CARD, False) + and config.get(CONF_UPDATE_INTERVAL, False) == SCHEDULER_DONT_RUN + ): + raise cv.Invalid( + f"`{CONF_SHOW_TEST_CARD}: True` cannot be used with `{CONF_UPDATE_INTERVAL}: never` because this combination will not show a test_card." + ) + return config + + FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( { cv.Optional(CONF_ROTATION): validate_rotation, @@ -95,6 +108,7 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean, } ) +FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card) async def setup_display_core_(var, config): @@ -201,7 +215,6 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg, page = await cg.get_variable(config[CONF_PAGE_ID]) var = cg.new_Pvariable(condition_id, template_arg, paren) cg.add(var.set_page(page)) - return var 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/dps310/dps310.cpp b/esphome/components/dps310/dps310.cpp index a7fb7ecd5e..6b6f9622fa 100644 --- a/esphome/components/dps310/dps310.cpp +++ b/esphome/components/dps310/dps310.cpp @@ -11,8 +11,6 @@ void DPS310Component::setup() { uint8_t coef_data_raw[DPS310_NUM_COEF_REGS]; auto timer = DPS310_INIT_TIMEOUT; uint8_t reg = 0; - - ESP_LOGCONFIG(TAG, "Running setup"); // first, reset the sensor if (!this->write_byte(DPS310_REG_RESET, DPS310_CMD_RESET)) { this->mark_failed(); diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index db0180e6f1..077db497b1 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -10,7 +10,6 @@ namespace ds1307 { static const char *const TAG = "ds1307"; void DS1307Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (!this->read_rtc_()) { this->mark_failed(); } diff --git a/esphome/components/ds2484/ds2484.cpp b/esphome/components/ds2484/ds2484.cpp index c3df9786b6..7c890ff433 100644 --- a/esphome/components/ds2484/ds2484.cpp +++ b/esphome/components/ds2484/ds2484.cpp @@ -5,7 +5,6 @@ namespace ds2484 { static const char *const TAG = "ds2484.onewire"; void DS2484OneWireBus::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->reset_device(); this->search(); } diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.cpp b/esphome/components/duty_cycle/duty_cycle_sensor.cpp index 8939de0ee9..40a728d025 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.cpp +++ b/esphome/components/duty_cycle/duty_cycle_sensor.cpp @@ -8,7 +8,6 @@ namespace duty_cycle { static const char *const TAG = "duty_cycle"; void DutyCycleSensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); this->pin_->setup(); this->store_.pin = this->pin_->to_isr(); this->store_.last_level = this->pin_->digital_read(); diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index bdaa3f3200..3a8a9b3725 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -16,7 +16,6 @@ static const uint16_t PRESSURE_ADDRESS = 0x04B0; void EE895Component::setup() { uint16_t crc16_check = 0; - ESP_LOGCONFIG(TAG, "Running setup"); write_command_(SERIAL_NUMBER, 8); uint8_t serial_number[20]; this->read(serial_number, 20); diff --git a/esphome/components/ektf2232/touchscreen/ektf2232.cpp b/esphome/components/ektf2232/touchscreen/ektf2232.cpp index 666e56e2a7..1dacee6a57 100644 --- a/esphome/components/ektf2232/touchscreen/ektf2232.cpp +++ b/esphome/components/ektf2232/touchscreen/ektf2232.cpp @@ -16,7 +16,6 @@ static const uint8_t GET_Y_RES[4] = {0x53, 0x63, 0x00, 0x00}; static const uint8_t GET_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01}; void EKTF2232Touchscreen::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); diff --git a/esphome/components/emc2101/emc2101.cpp b/esphome/components/emc2101/emc2101.cpp index 75d324c2bb..7d85cd31cf 100644 --- a/esphome/components/emc2101/emc2101.cpp +++ b/esphome/components/emc2101/emc2101.cpp @@ -57,8 +57,6 @@ static const uint8_t EMC2101_POLARITY_BIT = 1 << 4; float Emc2101Component::get_setup_priority() const { return setup_priority::HARDWARE; } void Emc2101Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // make sure we're talking to the right chip uint8_t chip_id = reg(EMC2101_REGISTER_WHOAMI).get(); if ((chip_id != EMC2101_CHIP_ID) && (chip_id != EMC2101_ALT_CHIP_ID)) { diff --git a/esphome/components/ens160_base/ens160_base.cpp b/esphome/components/ens160_base/ens160_base.cpp index 7e5b8528b7..6ffaac9588 100644 --- a/esphome/components/ens160_base/ens160_base.cpp +++ b/esphome/components/ens160_base/ens160_base.cpp @@ -49,8 +49,6 @@ static const uint8_t ENS160_DATA_STATUS_NEWGPR = 0x01; static const uint8_t ENS160_DATA_AQI = 0x07; void ENS160Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // check part_id uint16_t part_id; if (!this->read_bytes(ENS160_REG_PART_ID, reinterpret_cast(&part_id), 2)) { diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp index b296e9dd42..98a300f5d7 100644 --- a/esphome/components/ens210/ens210.cpp +++ b/esphome/components/ens210/ens210.cpp @@ -87,7 +87,6 @@ static uint32_t crc7(uint32_t value) { } void ENS210Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t data[2]; uint16_t part_id = 0; // Reset diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index bcbaf3d270..e5729703ed 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -38,8 +38,6 @@ void ES7210::dump_config() { } void ES7210::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Software reset ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0xff)); ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x32)); diff --git a/esphome/components/es7243e/es7243e.cpp b/esphome/components/es7243e/es7243e.cpp index d5115cb880..d45c1d5a8c 100644 --- a/esphome/components/es7243e/es7243e.cpp +++ b/esphome/components/es7243e/es7243e.cpp @@ -34,8 +34,6 @@ void ES7243E::dump_config() { } void ES7243E::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x3A)); ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x80)); ES7243E_ERROR_FAILED(this->write_byte(ES7243E_TEST_MODE_REGF9, 0x00)); diff --git a/esphome/components/es8156/es8156.cpp b/esphome/components/es8156/es8156.cpp index c8330b4f84..e84252efe2 100644 --- a/esphome/components/es8156/es8156.cpp +++ b/esphome/components/es8156/es8156.cpp @@ -17,8 +17,6 @@ static const char *const TAG = "es8156"; } void ES8156::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - ES8156_ERROR_FAILED(this->write_byte(ES8156_REG02_SCLK_MODE, 0x04)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG20_ANALOG_SYS1, 0x2A)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG21_ANALOG_SYS2, 0x3C)); diff --git a/esphome/components/es8311/es8311.cpp b/esphome/components/es8311/es8311.cpp index 0e59ac12d5..cf864187f9 100644 --- a/esphome/components/es8311/es8311.cpp +++ b/esphome/components/es8311/es8311.cpp @@ -22,8 +22,6 @@ static const char *const TAG = "es8311"; } void ES8311::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Reset ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x1F)); ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x00)); diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index 87cf9a47ee..69c16a9615 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -23,8 +23,6 @@ static const char *const TAG = "es8388"; } void ES8388::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // mute DAC this->set_mute_state_(true); diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index fdc469e419..c219b8851a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_FRAMEWORK, CONF_IGNORE_EFUSE_CUSTOM_MAC, CONF_IGNORE_EFUSE_MAC_CRC, + CONF_LOG_LEVEL, CONF_NAME, CONF_PATH, CONF_PLATFORM_VERSION, @@ -31,6 +32,7 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_ESP32, + ThreadModel, __version__, ) from esphome.core import CORE, HexInt, TimePeriod @@ -39,7 +41,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, @@ -75,8 +77,18 @@ CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" +CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_RELEASE = "release" +LOG_LEVELS_IDF = [ + "NONE", + "ERROR", + "WARN", + "INFO", + "DEBUG", + "VERBOSE", +] + ASSERTION_LEVELS = { "DISABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE", "ENABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE", @@ -309,19 +321,19 @@ def _format_framework_espidf_version( # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 3) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) # The platform-espressif32 version to use for arduino frameworks # - https://github.com/pioarduino/platform-espressif32/releases -ARDUINO_PLATFORM_VERSION = cv.Version(53, 3, 13) +ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 3, 2) +RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2) # The platformio/espressif32 version to use for esp-idf frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 -ESP_IDF_PLATFORM_VERSION = cv.Version(53, 3, 13) +ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2") # List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ @@ -356,8 +368,8 @@ SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(3, 1, 3), "https://github.com/espressif/arduino-esp32.git"), - "latest": (cv.Version(3, 1, 3), None), + "dev": (cv.Version(3, 2, 1), "https://github.com/espressif/arduino-esp32.git"), + "latest": (cv.Version(3, 2, 1), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } @@ -395,8 +407,8 @@ def _arduino_check_versions(value): def _esp_idf_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(5, 3, 2), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(5, 3, 2), None), + "dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"), + "latest": (cv.Version(5, 2, 2), None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), } @@ -467,10 +479,10 @@ def _parse_platform_version(value): try: ver = cv.Version.parse(cv.version_number(value)) if ver.major >= 50: # a pioarduino version - if "-" in value: - # maybe a release candidate?...definitely not our default, just use it as-is... - return f"https://github.com/pioarduino/platform-espressif32/releases/download/{value}/platform-espressif32.zip" - return f"https://github.com/pioarduino/platform-espressif32/releases/download/{ver.major}.{ver.minor:02d}.{ver.patch:02d}/platform-espressif32.zip" + release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}" + if ver.extra: + release += f"-{ver.extra}" + return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip" # if platform version is a valid version constraint, prefix the default package cv.platformio_version_constraint(value) return f"platformio/espressif32@{value}" @@ -487,56 +499,90 @@ 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 def final_validate(config): - if not ( - pio_options := fv.full_config.get()[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS) - ): - # Not specified or empty - return config - - pio_flash_size_key = "board_upload.flash_size" - pio_partitions_key = "board_build.partitions" - if CONF_PARTITIONS in config and pio_partitions_key in pio_options: - raise cv.Invalid( - f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32" - ) - - if pio_flash_size_key in pio_options: - raise cv.Invalid( - f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" - ) + # Imported locally to avoid circular import issues + from esphome.components.psram import DOMAIN as PSRAM_DOMAIN + errs = [] + full_config = fv.full_config.get() + if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS): + pio_flash_size_key = "board_upload.flash_size" + pio_partitions_key = "board_build.partitions" + if CONF_PARTITIONS in config and pio_partitions_key in pio_options: + errs.append( + cv.Invalid( + f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32" + ) + ) + if pio_flash_size_key in pio_options: + errs.append( + cv.Invalid( + f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" + ) + ) if ( config[CONF_VARIANT] != VARIANT_ESP32 and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK]) and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED] ): - raise cv.Invalid( - f"{CONF_IGNORE_EFUSE_MAC_CRC} is not supported on {config[CONF_VARIANT]}" + errs.append( + cv.Invalid( + f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC], + ) ) + if ( + config.get(CONF_FRAMEWORK, {}) + .get(CONF_ADVANCED, {}) + .get(CONF_EXECUTE_FROM_PSRAM) + ): + if config[CONF_VARIANT] != VARIANT_ESP32S3: + errs.append( + cv.Invalid( + f"'{CONF_EXECUTE_FROM_PSRAM}' is only supported on {VARIANT_ESP32S3} variant", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM], + ) + ) + if PSRAM_DOMAIN not in full_config: + errs.append( + cv.Invalid( + f"'{CONF_EXECUTE_FROM_PSRAM}' requires PSRAM to be configured", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM], + ) + ) + + if errs: + raise cv.MultipleInvalid(errs) return config @@ -563,6 +609,8 @@ CONF_SDKCONFIG_OPTIONS = "sdkconfig_options" CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server" CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" +CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking" +CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety" def _validate_idf_component(config: ConfigType) -> ConfigType: @@ -585,6 +633,9 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { cv.string_strict: cv.string_strict }, + cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( + *LOG_LEVELS_IDF, upper=True + ), cv.Optional(CONF_ADVANCED, default={}): cv.Schema( { cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( @@ -611,6 +662,13 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( cv.Optional( CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False ): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True + ): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True + ): cv.boolean, + cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, } ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( @@ -635,6 +693,64 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( ) +class _FrameworkMigrationWarning: + shown = False + + +def _show_framework_migration_message(name: str, variant: str) -> None: + """Show a friendly message about framework migration when defaulting to Arduino.""" + if _FrameworkMigrationWarning.shown: + return + _FrameworkMigrationWarning.shown = True + + from esphome.log import AnsiFore, color + + message = ( + color( + AnsiFore.BOLD_CYAN, + f"💡 IMPORTANT: {name} doesn't have a framework specified!", + ) + + "\n\n" + + f"Currently, {variant} defaults to the Arduino framework.\n" + + color(AnsiFore.YELLOW, "This will change to ESP-IDF in ESPHome 2026.1.0.\n") + + "\n" + + "Note: Newer ESP32 variants (C6, H2, P4, etc.) already use ESP-IDF by default.\n" + + "\n" + + "Why change? ESP-IDF offers:\n" + + color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n") + + color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n") + + color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n") + + color( + AnsiFore.GREEN, + " 🔧 Active development and testing by ESPHome developers\n", + ) + + "\n" + + "Trade-offs:\n" + + color(AnsiFore.YELLOW, " ⏱️ Compile times are ~25% longer\n") + + color(AnsiFore.YELLOW, " 🔄 Some components need migration\n") + + "\n" + + "What should I do?\n" + + color(AnsiFore.CYAN, " Option 1") + + ": Migrate to ESP-IDF (recommended)\n" + + " Add this to your YAML under 'esp32:':\n" + + color(AnsiFore.WHITE, " framework:\n") + + color(AnsiFore.WHITE, " type: esp-idf\n") + + "\n" + + color(AnsiFore.CYAN, " Option 2") + + ": Keep using Arduino (still supported)\n" + + " Add this to your YAML under 'esp32:':\n" + + color(AnsiFore.WHITE, " framework:\n") + + color(AnsiFore.WHITE, " type: arduino\n") + + "\n" + + "Need help? Check out the migration guide:\n" + + color( + AnsiFore.BLUE, + "https://esphome.io/guides/esp32_arduino_to_idf.html", + ) + ) + _LOGGER.warning(message) + + def _set_default_framework(config): if CONF_FRAMEWORK not in config: config = config.copy() @@ -643,6 +759,10 @@ def _set_default_framework(config): if variant in ARDUINO_ALLOWED_VARIANTS: config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({}) config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO + # Show the migration message + _show_framework_migration_message( + config.get(CONF_NAME, "This device"), variant + ) else: config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({}) config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF @@ -676,7 +796,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 +811,7 @@ CONFIG_SCHEMA = cv.All( _detect_variant, _set_default_framework, set_core_data, + cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT), ) @@ -705,6 +826,7 @@ 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(ThreadModel.MULTI_ATOMICS) cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_compat_mode", "strict") @@ -774,6 +896,21 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) + if advanced.get(CONF_EXECUTE_FROM_PSRAM, False): + add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) + + # Apply LWIP core locking for better socket performance + # This is already enabled by default in Arduino framework, where it provides + # significant performance benefits. Our benchmarks show socket operations are + # 24-200% faster with core locking enabled: + # - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default) + # - Up to 200% slower under load when all operations queue through tcpip_thread + # Enabling this makes ESP-IDF socket performance match Arduino framework. + if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True): + add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True) + if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True): + add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True) cg.add_platformio_option("board_build.partitions", "partitions.csv") if CONF_PARTITIONS in config: @@ -813,6 +950,10 @@ async def to_code(config): ), ) + add_idf_sdkconfig_option( + f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True + ) + for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) @@ -862,7 +1003,7 @@ def get_arduino_partition_csv(flash_size): eeprom_partition_start = app1_partition_start + app_partition_size spiffs_partition_start = eeprom_partition_start + eeprom_partition_size - partition_csv = f"""\ + return f"""\ nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xE000, 0x2000, app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X}, @@ -870,20 +1011,18 @@ app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X}, eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X}, spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X} """ - return partition_csv def get_idf_partition_csv(flash_size): app_partition_size = APP_PARTITION_SIZES[flash_size] - partition_csv = f"""\ + return f"""\ otadata, data, ota, , 0x2000, phy_init, data, phy, , 0x1000, app0, app, ota_0, , 0x{app_partition_size:X}, app1, app, ota_1, , 0x{app_partition_size:X}, nvs, data, nvs, , 0x6D000, """ - return partition_csv def _format_sdkconfig_val(value: SdkconfigValueType) -> str: @@ -943,14 +1082,16 @@ def _write_idf_component_yml(): # Called by writer.py def copy_files(): - if CORE.using_arduino: - if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_arduino_partition_csv( - CORE.platformio_options.get("board_upload.flash_size") - ), - ) + if ( + CORE.using_arduino + and "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] + ): + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_arduino_partition_csv( + CORE.platformio_options.get("board_upload.flash_size") + ), + ) if CORE.using_esp_idf: _write_sdkconfig() _write_idf_component_yml() @@ -970,7 +1111,7 @@ def copy_files(): __version__, ) - for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): + for file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].values(): if file[KEY_PATH].startswith("http"): import requests 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/gpio.py b/esphome/components/esp32/gpio.py index c35e5c2215..513f463d57 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -187,8 +187,7 @@ def validate_supports(value): "Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN] ) - value = _esp32_validations[variant].usage_validation(value) - return value + return _esp32_validations[variant].usage_validation(value) # https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/gpio.html#_CPPv416gpio_drive_cap_t diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index 7c3a658b17..f37297764b 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -2,6 +2,7 @@ import logging import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER +from esphome.pins import check_strapping_pin _ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21} @@ -15,13 +16,6 @@ _LOGGER = logging.getLogger(__name__) def esp32_h2_validate_gpio_pin(value): if value < 0 or value > 27: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)") - if value in _ESP32H2_STRAPPING_PINS: - _LOGGER.warning( - "GPIO%d is a Strapping PIN and should be avoided.\n" - "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" - "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", - value, - ) if value in _ESP32H2_SPI_FLASH_PINS: _LOGGER.warning( "GPIO%d is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n" @@ -49,4 +43,5 @@ def esp32_h2_validate_supports(value): if is_input: # All ESP32 pins support input mode pass + check_strapping_pin(value, _ESP32H2_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index 650d06e108..34d1b3139d 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -2,6 +2,7 @@ import logging import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER +from esphome.pins import check_strapping_pin _ESP32P4_USB_JTAG_PINS = {24, 25} @@ -13,13 +14,6 @@ _LOGGER = logging.getLogger(__name__) def esp32_p4_validate_gpio_pin(value): if value < 0 or value > 54: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") - if value in _ESP32P4_STRAPPING_PINS: - _LOGGER.warning( - "GPIO%d is a Strapping PIN and should be avoided.\n" - "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" - "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", - value, - ) if value in _ESP32P4_USB_JTAG_PINS: _LOGGER.warning( "GPIO%d is reserved for the USB-Serial-JTAG interface.\n" @@ -40,4 +34,5 @@ def esp32_p4_validate_supports(value): if is_input: # All ESP32 pins support input mode pass + check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index c181cf30b1..c995214232 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -1,77 +1,128 @@ -# Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664 +Import("env") # noqa: F821 -# pylint: disable=E0602 -Import("env") # noqa - -import os -import shutil - -if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: - try: - import esptool - except ImportError: - env.Execute("$PYTHONEXE -m pip install esptool") -else: - import subprocess -from SCons.Script import ARGUMENTS - -# Copy over the default sdkconfig. -from os import path - -if path.exists("./sdkconfig.defaults"): - os.makedirs(".temp", exist_ok=True) - shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf") +import itertools # noqa: E402 +import json # noqa: E402 +import os # noqa: E402 +import pathlib # noqa: E402 +import shutil # noqa: E402 -def esp32_create_combined_bin(source, target, env): - verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0"))) - if verbose: - print("Generating combined binary for serial flashing") - app_offset = 0x10000 +def merge_factory_bin(source, target, env): + """ + Merges all flash sections into a single .factory.bin using esptool. + Attempts multiple methods to detect image layout: flasher_args.json, FLASH_EXTRA_IMAGES, fallback guesses. + """ + firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin" + build_dir = pathlib.Path(env.subst("$BUILD_DIR")) + firmware_path = build_dir / firmware_name + flash_size = env.BoardConfig().get("upload.flash_size", "4MB") + chip = env.BoardConfig().get("build.mcu", "esp32") - new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin") - sections = env.subst(env.get("FLASH_EXTRA_IMAGES")) - firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") - chip = env.get("BOARD_MCU") - flash_size = env.BoardConfig().get("upload.flash_size") + sections = [] + flasher_args_path = build_dir / "flasher_args.json" + + # 1. Try flasher_args.json + if flasher_args_path.exists(): + try: + with flasher_args_path.open() as f: + flash_data = json.load(f) + for addr, fname in sorted( + flash_data["flash_files"].items(), key=lambda kv: int(kv[0], 16) + ): + file_path = pathlib.Path(fname) + if file_path.exists(): + sections.append((addr, str(file_path))) + else: + print(f"Info: {file_path.name} not found - skipping") + except Exception as e: + print(f"Warning: Failed to parse flasher_args.json - {e}") + + # 2. Try FLASH_EXTRA_IMAGES if flasher_args.json failed or was empty + if not sections: + flash_images = env.get("FLASH_EXTRA_IMAGES") + if flash_images: + print("Using FLASH_EXTRA_IMAGES from PlatformIO environment") + # flatten any nested lists + flat = list( + itertools.chain.from_iterable( + x if isinstance(x, (list, tuple)) else [x] for x in flash_images + ) + ) + entries = [env.subst(x) for x in flat] + for i in range(0, len(entries) - 1, 2): + addr, fname = entries[i], entries[i + 1] + if isinstance(fname, (list, tuple)): + print( + f"Warning: Skipping malformed FLASH_EXTRA_IMAGES entry: {fname}" + ) + continue + file_path = pathlib.Path(str(fname)) + if file_path.exists(): + sections.append((addr, file_path)) + else: + print(f"Info: {file_path.name} not found — skipping") + if sections: + # Append main firmware to sections + sections.append(("0x10000", firmware_path)) + + # 3. Final fallback: guess standard image locations + if not sections: + print("Fallback: guessing legacy image paths") + guesses = [ + ("0x0", build_dir / "bootloader" / "bootloader.bin"), + ("0x8000", build_dir / "partition_table" / "partition-table.bin"), + ("0xe000", build_dir / "ota_data_initial.bin"), + ("0x10000", firmware_path), + ] + for addr, file_path in guesses: + if file_path.exists(): + sections.append((addr, file_path)) + else: + print(f"Info: {file_path.name} not found — skipping") + + # If no valid sections found, skip merge + if not sections: + print("No valid flash sections found — skipping .factory.bin creation.") + return + + output_path = firmware_path.with_suffix(".factory.bin") + python_exe = f'"{env.subst("$PYTHONEXE")}"' cmd = [ + python_exe, + "-m", + "esptool", "--chip", chip, - "merge_bin", - "-o", - new_file_name, - "--flash_size", + "merge-bin", + "--flash-size", flash_size, + "--output", + str(output_path), ] - if verbose: - print(" Offset | File") - for section in sections: - sect_adr, sect_file = section.split(" ", 1) - if verbose: - print(f" - {sect_adr} | {sect_file}") - cmd += [sect_adr, sect_file] + for addr, file_path in sections: + cmd += [addr, str(file_path)] - cmd += [hex(app_offset), firmware_name] + print(f"Merging binaries into {output_path}") + result = env.Execute( + env.VerboseAction(" ".join(cmd), "Merging binaries with esptool") + ) - if verbose: - print(f" - {hex(app_offset)} | {firmware_name}") - print() - print(f"Using esptool.py arguments: {' '.join(cmd)}") - print() - - if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: - esptool.main(cmd) + if result == 0: + print(f"Successfully created {output_path}") else: - subprocess.run(["esptool.py", *cmd]) + print(f"Error: esptool merge-bin failed with code {result}") def esp32_copy_ota_bin(source, target, env): + """ + Copy the main firmware to a .ota.bin file for compatibility with ESPHome OTA tools. + """ firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin") new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.ota.bin") - shutil.copyfile(firmware_name, new_file_name) + print(f"Copied firmware to {new_file_name}") -# pylint: disable=E0602 -env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa -env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa +# Run merge first, then ota copy second +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821 +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821 diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 93bb643596..c758b3ef8f 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -6,12 +6,13 @@ import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant import esphome.config_validation as cv from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, CONF_NAME -from esphome.core import CORE +from esphome.core import CORE, TimePeriod from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX import esphome.final_validate as fv DEPENDENCIES = ["esp32"] -CODEOWNERS = ["@jesserockz", "@Rapsssito"] +CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] +DOMAIN = "esp32_ble" class BTLoggers(Enum): @@ -115,8 +116,11 @@ def register_bt_logger(*loggers: BTLoggers) -> None: CONF_BLE_ID = "ble_id" CONF_IO_CAPABILITY = "io_capability" +CONF_ADVERTISING = "advertising" CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time" CONF_DISABLE_BT_LOGS = "disable_bt_logs" +CONF_CONNECTION_TIMEOUT = "connection_timeout" +CONF_MAX_NOTIFICATIONS = "max_notifications" NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] @@ -161,12 +165,23 @@ CONFIG_SCHEMA = cv.Schema( IO_CAPABILITY, lower=True ), cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, + cv.Optional(CONF_ADVERTISING, default=False): cv.boolean, cv.Optional( CONF_ADVERTISING_CYCLE_TIME, default="10s" ): cv.positive_time_period_milliseconds, cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All( cv.only_with_esp_idf, cv.boolean ), + cv.SplitDefault(CONF_CONNECTION_TIMEOUT, esp32_idf="20s"): cv.All( + cv.only_with_esp_idf, + cv.positive_time_period_seconds, + cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)), + ), + cv.SplitDefault(CONF_MAX_NOTIFICATIONS, esp32_idf=12): cv.All( + cv.only_with_esp_idf, + cv.positive_int, + cv.Range(min=1, max=64), + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -255,8 +270,31 @@ async def to_code(config): if logger not in _required_loggers: add_idf_sdkconfig_option(f"{logger.value}_NONE", True) + # Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector + # Default is 20 seconds instead of ESP-IDF's 30 seconds. Because there is no way to + # cancel a BLE connection in progress, when aioesphomeapi times out at 20 seconds, + # the connection slot remains occupied for the remaining time, preventing new connection + # attempts and wasting valuable connection slots. + if CONF_CONNECTION_TIMEOUT in config: + timeout_seconds = int(config[CONF_CONNECTION_TIMEOUT].total_seconds) + add_idf_sdkconfig_option( + "CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds + ) + + # Set the maximum number of notification registrations + # This controls how many BLE characteristics can have notifications enabled + # across all connections for a single GATT client interface + # https://github.com/esphome/issues/issues/6808 + if CONF_MAX_NOTIFICATIONS in config: + add_idf_sdkconfig_option( + "CONFIG_BT_GATTC_NOTIF_REG_MAX", config[CONF_MAX_NOTIFICATIONS] + ) + cg.add_define("USE_ESP32_BLE") + if config[CONF_ADVERTISING]: + cg.add_define("USE_ESP32_BLE_ADVERTISING") + @automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({})) async def ble_enabled_to_code(config, condition_id, template_arg, args): diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 8b0cf4da98..d1ee7af4ea 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -1,7 +1,7 @@ -#ifdef USE_ESP32 - #include "ble.h" +#ifdef USE_ESP32 + #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -19,15 +19,12 @@ #include #endif -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; void ESP32BLE::setup() { global_ble = this; - ESP_LOGCONFIG(TAG, "Running setup"); - if (!ble_pre_setup_()) { ESP_LOGE(TAG, "BLE could not be prepared for configuration"); this->mark_failed(); @@ -56,6 +53,7 @@ void ESP32BLE::disable() { bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; } +#ifdef USE_ESP32_BLE_ADVERTISING void ESP32BLE::advertising_start() { this->advertising_init_(); if (!this->is_active()) @@ -91,6 +89,7 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) { this->advertising_->remove_service_uuid(uuid); this->advertising_start(); } +#endif bool ESP32BLE::ble_pre_setup_() { esp_err_t err = nvs_flash_init(); @@ -101,6 +100,7 @@ bool ESP32BLE::ble_pre_setup_() { return true; } +#ifdef USE_ESP32_BLE_ADVERTISING void ESP32BLE::advertising_init_() { if (this->advertising_ != nullptr) return; @@ -110,6 +110,7 @@ void ESP32BLE::advertising_init_() { this->advertising_->set_min_preferred_interval(0x06); this->advertising_->set_appearance(this->appearance_); } +#endif bool ESP32BLE::ble_setup_() { esp_err_t err; @@ -397,9 +398,11 @@ void ESP32BLE::loop() { this->ble_event_pool_.release(ble_event); ble_event = this->ble_events_.pop(); } +#ifdef USE_ESP32_BLE_ADVERTISING if (this->advertising_ != nullptr) { this->advertising_->loop(); } +#endif // Log dropped events periodically uint16_t dropped = this->ble_events_.get_and_reset_dropped_count(); @@ -471,6 +474,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa // Ignore these GAP events as they are not relevant for our use case case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: + case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete + case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm return; default: @@ -538,7 +543,6 @@ uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble #endif diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 2c5697df82..712787fe53 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -1,14 +1,17 @@ #pragma once -#include "ble_advertising.h" +#include "esphome/core/defines.h" // Must be included before conditional includes + #include "ble_uuid.h" #include "ble_scan_result.h" +#ifdef USE_ESP32_BLE_ADVERTISING +#include "ble_advertising.h" +#endif #include #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "ble_event.h" @@ -21,24 +24,16 @@ #include #include -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { -// Maximum number of BLE scan results to buffer -// Sized to handle bursts of advertisements while allowing for processing delays -// With 16 advertisements per batch and some safety margin: -// - Without PSRAM: 24 entries (1.5× batch size) -// - With PSRAM: 36 entries (2.25× batch size) -// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers +// Maximum size of the BLE event queue +// Increased to absorb the ring buffer capacity from esp32_ble_tracker #ifdef USE_PSRAM -static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36; +static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100; // 64 + 36 (ring buffer size with PSRAM) #else -static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24; +static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88; // 64 + 24 (ring buffer size without PSRAM) #endif -// Maximum size of the BLE event queue - must be power of 2 for lock-free queue -static constexpr size_t MAX_BLE_QUEUE_SIZE = 64; - uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); // NOLINTNEXTLINE(modernize-use-using) @@ -114,6 +109,7 @@ class ESP32BLE : public Component { float get_setup_priority() const override; void set_name(const std::string &name) { this->name_ = name; } +#ifdef USE_ESP32_BLE_ADVERTISING void advertising_start(); void advertising_set_service_data(const std::vector &data); void advertising_set_manufacturer_data(const std::vector &data); @@ -121,6 +117,7 @@ class ESP32BLE : public Component { void advertising_add_service_uuid(ESPBTUUID uuid); void advertising_remove_service_uuid(ESPBTUUID uuid); void advertising_register_raw_advertisement_callback(std::function &&callback); +#endif void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } void register_gap_scan_event_handler(GAPScanEventHandler *handler) { @@ -141,7 +138,9 @@ class ESP32BLE : public Component { bool ble_setup_(); bool ble_dismantle_(); bool ble_pre_setup_(); +#ifdef USE_ESP32_BLE_ADVERTISING void advertising_init_(); +#endif private: template friend void enqueue_ble_event(Args... args); @@ -161,7 +160,9 @@ class ESP32BLE : public Component { optional name_; // 4-byte aligned members - BLEAdvertising *advertising_{}; // 4 bytes (pointer) +#ifdef USE_ESP32_BLE_ADVERTISING + BLEAdvertising *advertising_{}; // 4 bytes (pointer) +#endif esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) uint32_t advertising_cycle_time_{}; // 4 bytes @@ -191,7 +192,6 @@ template class BLEDisableAction : public Action { void play(Ts... x) override { global_ble->disable(); } }; -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble #endif diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index 8d43b5af33..d8b9b1cc36 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -1,6 +1,7 @@ #include "ble_advertising.h" #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_ADVERTISING #include #include @@ -8,8 +9,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble.advertising"; @@ -160,7 +160,7 @@ void BLEAdvertising::register_raw_advertisement_callback(std::functionraw_advertisements_callbacks_.push_back(std::move(callback)); } -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble -#endif +#endif // USE_ESP32_BLE_ADVERTISING +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index 0b2142115d..e373554ea9 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -1,17 +1,19 @@ #pragma once +#include "esphome/core/defines.h" + #include #include #include #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_ADVERTISING #include #include #include -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { using raw_adv_data_t = struct { uint8_t *data; @@ -55,7 +57,7 @@ class BLEAdvertising { int8_t current_adv_index_{-1}; // -1 means standard scan response }; -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble -#endif +#endif // USE_ESP32_BLE_ADVERTISING +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index 9268c710f3..884fc9ba65 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -11,8 +11,7 @@ #include "ble_scan_result.h" -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { // Compile-time verification that ESP-IDF scan complete events only contain a status field // This ensures our reinterpret_cast in ble.cpp is safe @@ -395,7 +394,6 @@ static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScan // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble #endif diff --git a/esphome/components/esp32_ble/ble_scan_result.h b/esphome/components/esp32_ble/ble_scan_result.h index 49b0d5523d..980b39b0b2 100644 --- a/esphome/components/esp32_ble/ble_scan_result.h +++ b/esphome/components/esp32_ble/ble_scan_result.h @@ -4,8 +4,7 @@ #include -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { // Structure for BLE scan results - only fields we actually use struct __attribute__((packed)) BLEScanResult { @@ -18,7 +17,6 @@ struct __attribute__((packed)) BLEScanResult { uint8_t search_evt; }; // ~73 bytes vs ~400 bytes for full esp_ble_gap_cb_param_t -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble #endif diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index aa1edd96b2..be9c6945d7 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -1,14 +1,14 @@ #include "ble_uuid.h" #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_UUID #include #include #include #include "esphome/core/log.h" -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; @@ -189,7 +189,7 @@ std::string ESPBTUUID::to_string() const { return ""; } -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble -#endif +#endif // USE_ESP32_BLE_UUID +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index 06f84d4da7..4cf2d10abd 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -1,15 +1,16 @@ #pragma once +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_UUID #include #include -namespace esphome { -namespace esp32_ble { +namespace esphome::esp32_ble { class ESPBTUUID { public: @@ -41,7 +42,7 @@ class ESPBTUUID { esp_bt_uuid_t uuid_; }; -} // namespace esp32_ble -} // namespace esphome +} // namespace esphome::esp32_ble -#endif +#endif // USE_ESP32_BLE_UUID +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index 6e0d103aa0..8fc4fe941d 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -65,6 +65,8 @@ FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant async def to_code(config): + cg.add_define("USE_ESP32_BLE_UUID") + uuid = config[CONF_UUID].hex uuid_arr = [ cg.RawExpression(f"0x{uuid[i : i + 2]}") for i in range(0, len(uuid), 2) @@ -82,6 +84,8 @@ async def to_code(config): cg.add(var.set_measured_power(config[CONF_MEASURED_POWER])) cg.add(var.set_tx_power(config[CONF_TX_POWER])) + cg.add_define("USE_ESP32_BLE_ADVERTISING") + if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) diff --git a/esphome/components/esp32_ble_client/__init__.py b/esphome/components/esp32_ble_client/__init__.py index 25957ed0da..55619f1fc0 100644 --- a/esphome/components/esp32_ble_client/__init__.py +++ b/esphome/components/esp32_ble_client/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components import esp32_ble_tracker AUTO_LOAD = ["esp32_ble_tracker"] -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@bdraco"] DEPENDENCIES = ["esp32"] esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client") diff --git a/esphome/components/esp32_ble_client/ble_characteristic.cpp b/esphome/components/esp32_ble_client/ble_characteristic.cpp index 2fd7fe9871..36229c23c3 100644 --- a/esphome/components/esp32_ble_client/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_client/ble_characteristic.cpp @@ -5,9 +5,9 @@ #include "esphome/core/log.h" #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_DEVICE -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { static const char *const TAG = "esp32_ble_client"; @@ -93,7 +93,7 @@ esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size) return write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP); } -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client +#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_characteristic.h b/esphome/components/esp32_ble_client/ble_characteristic.h index a014788e65..1428b42739 100644 --- a/esphome/components/esp32_ble_client/ble_characteristic.h +++ b/esphome/components/esp32_ble_client/ble_characteristic.h @@ -1,6 +1,9 @@ #pragma once +#include "esphome/core/defines.h" + #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_DEVICE #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" @@ -8,8 +11,7 @@ #include -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -33,7 +35,7 @@ class BLECharacteristic { BLEService *service; }; -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client +#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 7d0a3bbfd5..2d84436d84 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -5,10 +5,27 @@ #ifdef USE_ESP32 -namespace esphome { -namespace esp32_ble_client { +#include +#include + +namespace esphome::esp32_ble_client { static const char *const TAG = "esp32_ble_client"; + +// Intermediate connection parameters for standard operation +// ESP-IDF defaults (12.5-15ms) are too slow for stable connections through WiFi-based BLE proxies, +// causing disconnections. These medium parameters balance responsiveness with bandwidth usage. +static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms +static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms +// The timeout value was increased from 6s to 8s to address stability issues observed +// in certain BLE devices when operating through WiFi-based BLE proxies. The longer +// timeout reduces the likelihood of disconnections during periods of high latency. +static const uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s + +// Fastest connection parameters for devices with short discovery timeouts +static const uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) +static const uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms +static const uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s static const esp_bt_uuid_t NOTIFY_DESC_UUID = { .len = ESP_UUID_LEN_16, .uuid = @@ -27,8 +44,10 @@ void BLEClientBase::set_state(espbt::ClientState st) { ESPBTClient::set_state(st); if (st == espbt::ClientState::READY_TO_CONNECT) { - // Enable loop when we need to connect + // Enable loop for state processing this->enable_loop(); + // Connect immediately instead of waiting for next loop + this->connect(); } } @@ -45,11 +64,6 @@ void BLEClientBase::loop() { } this->set_state(espbt::ClientState::IDLE); } - // READY_TO_CONNECT means we have discovered the device - // and the scanner has been stopped by the tracker. - else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { - this->connect(); - } // If its idle, we can disable the loop as set_state // will enable it again when we need to connect. else if (this->state_ == espbt::ClientState::IDLE) { @@ -64,40 +78,7 @@ void BLEClientBase::dump_config() { " Address: %s\n" " Auto-Connect: %s", this->address_str().c_str(), TRUEFALSE(this->auto_connect_)); - std::string state_name; - switch (this->state()) { - case espbt::ClientState::INIT: - state_name = "INIT"; - break; - case espbt::ClientState::DISCONNECTING: - state_name = "DISCONNECTING"; - break; - case espbt::ClientState::IDLE: - state_name = "IDLE"; - break; - case espbt::ClientState::SEARCHING: - state_name = "SEARCHING"; - break; - case espbt::ClientState::DISCOVERED: - state_name = "DISCOVERED"; - break; - case espbt::ClientState::READY_TO_CONNECT: - state_name = "READY_TO_CONNECT"; - break; - case espbt::ClientState::CONNECTING: - state_name = "CONNECTING"; - break; - case espbt::ClientState::CONNECTED: - state_name = "CONNECTED"; - break; - case espbt::ClientState::ESTABLISHED: - state_name = "ESTABLISHED"; - break; - default: - state_name = "UNKNOWN_STATE"; - break; - } - ESP_LOGCONFIG(TAG, " State: %s", state_name.c_str()); + ESP_LOGCONFIG(TAG, " State: %s", espbt::client_state_to_string(this->state())); if (this->status_ == ESP_GATT_NO_RESOURCES) { ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config."); } else if (this->status_ != ESP_GATT_OK) { @@ -105,6 +86,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,15 +104,46 @@ 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(), + ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_.c_str(), this->remote_addr_type_); this->paired_ = false; + + // Set preferred connection parameters before connecting + // Use FAST for all V3 connections (better latency and reliability) + // Use MEDIUM for V1/legacy connections (balanced performance) + uint16_t min_interval, max_interval, timeout; + const char *param_type; + + if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE || + this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { + min_interval = FAST_MIN_CONN_INTERVAL; + max_interval = FAST_MAX_CONN_INTERVAL; + timeout = FAST_CONN_TIMEOUT; + param_type = "fast"; + } else { + min_interval = MEDIUM_MIN_CONN_INTERVAL; + max_interval = MEDIUM_MAX_CONN_INTERVAL; + timeout = MEDIUM_CONN_TIMEOUT; + param_type = "medium"; + } + + auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval, + 0, // latency: 0 + timeout); + if (param_ret != ESP_OK) { + ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_, + this->address_str_.c_str(), param_ret); + } else { + this->log_connection_params_(param_type); + } + + // Now open the connection auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true); if (ret) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(), - ret); + this->log_gattc_warning_("esp_ble_gattc_open", ret); this->set_state(espbt::ClientState::IDLE); } else { this->set_state(espbt::ClientState::CONNECTING); @@ -140,14 +153,9 @@ void BLEClientBase::connect() { esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); } void BLEClientBase::disconnect() { - if (this->state_ == espbt::ClientState::IDLE) { - ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already idle.", this->connection_index_, - this->address_str_.c_str()); - return; - } - if (this->state_ == espbt::ClientState::DISCONNECTING) { - ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already disconnecting.", this->connection_index_, - this->address_str_.c_str()); + if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) { + ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_.c_str(), + espbt::client_state_to_string(this->state_)); return; } if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) { @@ -182,8 +190,7 @@ void BLEClientBase::unconditional_disconnect() { // In the future we might consider App.reboot() here since // the BLE stack is in an indeterminate state. // - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_close error, err=%d", this->connection_index_, this->address_str_.c_str(), - err); + this->log_gattc_warning_("esp_ble_gattc_close", err); } if (this->state_ == espbt::ClientState::SEARCHING || this->state_ == espbt::ClientState::READY_TO_CONNECT || @@ -196,9 +203,11 @@ void BLEClientBase::unconditional_disconnect() { } void BLEClientBase::release_services() { +#ifdef USE_ESP32_BLE_DEVICE for (auto &svc : this->services_) delete svc; // NOLINT(cppcoreguidelines-owning-memory) this->services_.clear(); +#endif #ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH esp_ble_gattc_cache_clean(this->remote_bda_); #endif @@ -208,6 +217,36 @@ void BLEClientBase::log_event_(const char *name) { ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name); } +void BLEClientBase::log_gattc_event_(const char *name) { + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_.c_str(), name); +} + +void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) { + ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, + status); +} + +void BLEClientBase::log_gattc_warning_(const char *operation, esp_err_t err) { + ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, err); +} + +void BLEClientBase::log_connection_params_(const char *param_type) { + ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type); +} + +void BLEClientBase::restore_medium_conn_params_() { + // Restore to medium connection parameters after initial connection phase + // This balances performance with bandwidth usage for normal operation + esp_ble_conn_update_params_t conn_params = {{0}}; + memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); + conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL; + conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL; + conn_params.latency = 0; + conn_params.timeout = MEDIUM_CONN_TIMEOUT; + this->log_connection_params_("medium"); + esp_ble_gap_update_conn_params(&conn_params); +} + bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, esp_ble_gattc_cb_param_t *param) { if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) @@ -235,30 +274,18 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_OPEN_EVT: { if (!this->check_addr(param->open.remote_bda)) return false; - this->log_event_("ESP_GATTC_OPEN_EVT"); - this->conn_id_ = param->open.conn_id; + this->log_gattc_event_("OPEN"); + // conn_id was already set in ESP_GATTC_CONNECT_EVT this->service_count_ = 0; if (this->state_ != espbt::ClientState::CONNECTING) { // This should not happen but lets log it in case it does // because it means we have a bad assumption about how the // ESP BT stack works. - if (this->state_ == espbt::ClientState::CONNECTED) { - ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already connected, status=%d", this->connection_index_, - this->address_str_.c_str(), param->open.status); - } else if (this->state_ == espbt::ClientState::ESTABLISHED) { - ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already established, status=%d", - this->connection_index_, this->address_str_.c_str(), param->open.status); - } else if (this->state_ == espbt::ClientState::DISCONNECTING) { - ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while disconnecting, status=%d", this->connection_index_, - this->address_str_.c_str(), param->open.status); - } else { - ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while not in connecting state, status=%d", - this->connection_index_, this->address_str_.c_str(), param->open.status); - } + ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while in %s state, status=%d", this->connection_index_, + this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status); } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { - ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(), - param->open.status); + this->log_gattc_warning_("Connection open", param->open.status); this->set_state(espbt::ClientState::IDLE); break; } @@ -270,32 +297,47 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->conn_id_ = UNSET_CONN_ID; break; } - auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id); - if (ret) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_, - this->address_str_.c_str(), ret); - } + // MTU negotiation already started in ESP_GATTC_CONNECT_EVT this->set_state(espbt::ClientState::CONNECTED); + ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str()); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); + // Restore to medium connection parameters for cached connections too + this->restore_medium_conn_params_(); // only set our state, subclients might have more stuff to do yet. this->state_ = espbt::ClientState::ESTABLISHED; break; } + ESP_LOGD(TAG, "[%d] [%s] Searching for services", this->connection_index_, this->address_str_.c_str()); esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); break; } case ESP_GATTC_CONNECT_EVT: { if (!this->check_addr(param->connect.remote_bda)) return false; - this->log_event_("ESP_GATTC_CONNECT_EVT"); + this->log_gattc_event_("CONNECT"); + this->conn_id_ = param->connect.conn_id; + // Start MTU negotiation immediately as recommended by ESP-IDF examples + // (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in + // ESP_GATTC_CONNECT_EVT instead of waiting for ESP_GATTC_OPEN_EVT. + // This saves ~3ms in the connection process. + auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id); + if (ret) { + this->log_gattc_warning_("esp_ble_gattc_send_mtu_req", ret); + } break; } case ESP_GATTC_DISCONNECT_EVT: { if (!this->check_addr(param->disconnect.remote_bda)) return false; - ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_, - this->address_str_.c_str(), param->disconnect.reason); + // Check if we were disconnected while waiting for service discovery + if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER && + this->state_ == espbt::ClientState::CONNECTED) { + ESP_LOGW(TAG, "[%d] [%s] Disconnected by remote during service discovery", this->connection_index_, + this->address_str_.c_str()); + } else { + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, + this->address_str_.c_str(), param->disconnect.reason); + } this->release_services(); this->set_state(espbt::ClientState::IDLE); break; @@ -318,7 +360,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_CLOSE_EVT: { if (this->conn_id_ != param->close.conn_id) return false; - this->log_event_("ESP_GATTC_CLOSE_EVT"); + this->log_gattc_event_("CLOSE"); this->release_services(); this->set_state(espbt::ClientState::IDLE); this->conn_id_ = UNSET_CONN_ID; @@ -330,63 +372,74 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->service_count_++; if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { // V3 clients don't need services initialized since - // they only request by handle after receiving the services. + // as they use the ESP APIs to get services. break; } +#ifdef USE_ESP32_BLE_DEVICE BLEService *ble_service = new BLEService(); // NOLINT(cppcoreguidelines-owning-memory) ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid); ble_service->start_handle = param->search_res.start_handle; ble_service->end_handle = param->search_res.end_handle; ble_service->client = this; this->services_.push_back(ble_service); +#endif break; } case ESP_GATTC_SEARCH_CMPL_EVT: { if (this->conn_id_ != param->search_cmpl.conn_id) return false; - this->log_event_("ESP_GATTC_SEARCH_CMPL_EVT"); - for (auto &svc : this->services_) { - ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(), - svc->uuid.to_string().c_str()); - ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, - this->address_str_.c_str(), svc->start_handle, svc->end_handle); + this->log_gattc_event_("SEARCH_CMPL"); + // For V3 connections, restore to medium connection parameters after service discovery + // This balances performance with bandwidth usage after the critical discovery phase + if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE || + this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { + this->restore_medium_conn_params_(); + } else { +#ifdef USE_ESP32_BLE_DEVICE + for (auto &svc : this->services_) { + ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(), + svc->uuid.to_string().c_str()); + ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, + this->address_str_.c_str(), svc->start_handle, svc->end_handle); + } +#endif } - ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); + ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str()); this->state_ = espbt::ClientState::ESTABLISHED; break; } case ESP_GATTC_READ_DESCR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_event_("ESP_GATTC_READ_DESCR_EVT"); + this->log_gattc_event_("READ_DESCR"); break; } case ESP_GATTC_WRITE_DESCR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_event_("ESP_GATTC_WRITE_DESCR_EVT"); + this->log_gattc_event_("WRITE_DESCR"); break; } case ESP_GATTC_WRITE_CHAR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - this->log_event_("ESP_GATTC_WRITE_CHAR_EVT"); + this->log_gattc_event_("WRITE_CHAR"); break; } case ESP_GATTC_READ_CHAR_EVT: { if (this->conn_id_ != param->read.conn_id) return false; - this->log_event_("ESP_GATTC_READ_CHAR_EVT"); + this->log_gattc_event_("READ_CHAR"); break; } case ESP_GATTC_NOTIFY_EVT: { if (this->conn_id_ != param->notify.conn_id) return false; - this->log_event_("ESP_GATTC_NOTIFY_EVT"); + this->log_gattc_event_("NOTIFY"); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - this->log_event_("ESP_GATTC_REG_FOR_NOTIFY_EVT"); + this->log_gattc_event_("REG_FOR_NOTIFY"); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { // Client is responsible for flipping the descriptor value @@ -398,8 +451,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle( this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, NOTIFY_DESC_UUID, &desc_result, &count); if (descr_status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_, - this->address_str_.c_str(), descr_status); + this->log_gattc_warning_("esp_ble_gattc_get_descr_by_char_handle", descr_status); break; } esp_gattc_char_elem_t char_result; @@ -407,8 +459,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, param->reg_for_notify.handle, &char_result, &count, 0); if (char_status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, - this->address_str_.c_str(), char_status); + this->log_gattc_warning_("esp_ble_gattc_get_all_char", char_status); break; } @@ -422,8 +473,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties); if (status) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_, - this->address_str_.c_str(), status); + this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status); } break; } @@ -531,6 +581,7 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { return NAN; } +#ifdef USE_ESP32_BLE_DEVICE BLEService *BLEClientBase::get_service(espbt::ESPBTUUID uuid) { for (auto *svc : this->services_) { if (svc->uuid == uuid) @@ -607,8 +658,8 @@ BLEDescriptor *BLEClientBase::get_descriptor(uint16_t handle) { } return nullptr; } +#endif // USE_ESP32_BLE_DEVICE -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index bf3b589b1b..2d00688dbd 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -5,7 +5,9 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/core/component.h" +#ifdef USE_ESP32_BLE_DEVICE #include "ble_service.h" +#endif #include #include @@ -16,8 +18,7 @@ #include #include -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -31,7 +32,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; @@ -46,7 +49,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; } - void set_address(uint64_t address) { + virtual void set_address(uint64_t address) { this->address_ = address; this->remote_bda_[0] = (address >> 40) & 0xFF; this->remote_bda_[1] = (address >> 32) & 0xFF; @@ -64,8 +67,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { (uint8_t) (this->address_ >> 0) & 0xff); } } - std::string address_str() const { return this->address_str_; } + const std::string &address_str() const { return this->address_str_; } +#ifdef USE_ESP32_BLE_DEVICE BLEService *get_service(espbt::ESPBTUUID uuid); BLEService *get_service(uint16_t uuid); BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr); @@ -76,6 +80,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { BLEDescriptor *get_descriptor(uint16_t handle); // Get the configuration descriptor for the given characteristic handle. BLEDescriptor *get_config_descriptor(uint16_t handle); +#endif float parse_char_value(uint8_t *value, uint16_t length); @@ -102,7 +107,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { // Group 2: Container types (grouped for memory optimization) std::string address_str_{}; +#ifdef USE_ESP32_BLE_DEVICE std::vector services_; +#endif // Group 3: 4-byte types int gattc_if_; @@ -125,9 +132,13 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { // 6 bytes used, 2 bytes padding void log_event_(const char *name); + void log_gattc_event_(const char *name); + void restore_medium_conn_params_(); + void log_gattc_warning_(const char *operation, esp_gatt_status_t status); + void log_gattc_warning_(const char *operation, esp_err_t err); + void log_connection_params_(const char *param_type); }; -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_descriptor.h b/esphome/components/esp32_ble_client/ble_descriptor.h index c05430144f..fb2b78a7b1 100644 --- a/esphome/components/esp32_ble_client/ble_descriptor.h +++ b/esphome/components/esp32_ble_client/ble_descriptor.h @@ -1,11 +1,13 @@ #pragma once +#include "esphome/core/defines.h" + #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_DEVICE #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -19,7 +21,7 @@ class BLEDescriptor { BLECharacteristic *characteristic; }; -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client +#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_service.cpp b/esphome/components/esp32_ble_client/ble_service.cpp index b22d2a1788..accaad15e1 100644 --- a/esphome/components/esp32_ble_client/ble_service.cpp +++ b/esphome/components/esp32_ble_client/ble_service.cpp @@ -4,9 +4,9 @@ #include "esphome/core/log.h" #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_DEVICE -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { static const char *const TAG = "esp32_ble_client"; @@ -71,7 +71,7 @@ void BLEService::parse_characteristics() { } } -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client +#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_client/ble_service.h b/esphome/components/esp32_ble_client/ble_service.h index 41fc3e838b..00ecc777e7 100644 --- a/esphome/components/esp32_ble_client/ble_service.h +++ b/esphome/components/esp32_ble_client/ble_service.h @@ -1,6 +1,9 @@ #pragma once +#include "esphome/core/defines.h" + #ifdef USE_ESP32 +#ifdef USE_ESP32_BLE_DEVICE #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" @@ -8,8 +11,7 @@ #include -namespace esphome { -namespace esp32_ble_client { +namespace esphome::esp32_ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -30,7 +32,7 @@ class BLEService { BLECharacteristic *get_characteristic(uint16_t uuid); }; -} // namespace esp32_ble_client -} // namespace esphome +} // namespace esphome::esp32_ble_client +#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 773445a1a7..8ddb15a7f8 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -140,20 +140,22 @@ VALUE_TYPES = { def validate_char_on_write(char_config): - if CONF_ON_WRITE in char_config: - if not char_config[CONF_WRITE] and not char_config[CONF_WRITE_NO_RESPONSE]: - raise cv.Invalid( - f"{CONF_ON_WRITE} requires the {CONF_WRITE} or {CONF_WRITE_NO_RESPONSE} property to be set" - ) + if ( + CONF_ON_WRITE in char_config + and not char_config[CONF_WRITE] + and not char_config[CONF_WRITE_NO_RESPONSE] + ): + raise cv.Invalid( + f"{CONF_ON_WRITE} requires the {CONF_WRITE} or {CONF_WRITE_NO_RESPONSE} property to be set" + ) return char_config def validate_descriptor(desc_config): - if CONF_ON_WRITE in desc_config: - if not desc_config[CONF_WRITE]: - raise cv.Invalid( - f"{CONF_ON_WRITE} requires the {CONF_WRITE} property to be set" - ) + if CONF_ON_WRITE in desc_config and not desc_config[CONF_WRITE]: + raise cv.Invalid( + f"{CONF_ON_WRITE} requires the {CONF_WRITE} property to be set" + ) if CONF_MAX_LENGTH not in desc_config: value = desc_config[CONF_VALUE][CONF_DATA] if cg.is_template(value): @@ -527,6 +529,7 @@ async def to_code_characteristic(service_var, char_conf): async def to_code(config): # Register the loggers this component needs esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP) + cg.add_define("USE_ESP32_BLE_UUID") var = cg.new_Pvariable(config[CONF_ID]) @@ -569,6 +572,7 @@ async def to_code(config): config[CONF_ON_DISCONNECT], ) cg.add_define("USE_ESP32_BLE_SERVER") + cg.add_define("USE_ESP32_BLE_ADVERTISING") if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) @@ -626,5 +630,4 @@ async def ble_server_descriptor_set_value(config, action_id, template_arg, args) ) async def ble_server_characteristic_notify(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 547cf84ed1..9ad2f3b25f 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -30,10 +30,13 @@ from esphome.const import ( CONF_SERVICE_UUID, CONF_TRIGGER_ID, ) -from esphome.core import CORE +from esphome.core import CORE, coroutine_with_priority +from esphome.enum import StrEnum +from esphome.types import ConfigType AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] +CODEOWNERS = ["@bdraco"] KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker" KEY_USED_CONNECTION_SLOTS = "used_connection_slots" @@ -50,6 +53,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,12 +299,19 @@ 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: - addr_list = [] - for it in conf[CONF_MAC_ADDRESS]: - addr_list.append(it.as_hex) + addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]] cg.add(trigger.set_addresses(addr_list)) await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): @@ -326,18 +355,27 @@ async def to_code(config): add_idf_sdkconfig_option( "CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS] ) - # CONFIG_BT_GATTC_NOTIF_REG_MAX controls the number of - # max notifications in 5.x, setting CONFIG_BT_ACL_CONNECTIONS - # is enough in 4.x - # https://github.com/esphome/issues/issues/6808 - add_idf_sdkconfig_option("CONFIG_BT_GATTC_NOTIF_REG_MAX", 9) cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") + + CORE.add_job(_add_ble_features) + if config.get(CONF_SOFTWARE_COEXISTENCE): cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE") +# This needs to be run as a job with very low priority so that all components have +# chance to call register_ble_tracker and register_client before the list is checked +# and added to the global defines list. +@coroutine_with_priority(-1000) +async def _add_ble_features(): + # Add feature-specific defines based on what's needed + if BLEFeatures.ESP_BT_DEVICE in _required_features: + cg.add_define("USE_ESP32_BLE_DEVICE") + cg.add_define("USE_ESP32_BLE_UUID") + + ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(ESP32BLETracker), @@ -382,13 +420,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..c0e6eee138 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -5,8 +5,8 @@ #ifdef USE_ESP32 -namespace esphome { -namespace esp32_ble_tracker { +namespace esphome::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 +87,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: @@ -106,7 +107,6 @@ template class ESP32BLEStopScanAction : public Action, pu void play(Ts... x) override { this->parent_->stop_scan(); } }; -} // namespace esp32_ble_tracker -} // namespace esphome +} // namespace esphome::esp32_ble_tracker #endif diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index d950ccb5f1..0455d136df 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -35,13 +35,37 @@ // bt_trace.h #undef TAG -namespace esphome { -namespace esp32_ble_tracker { +namespace esphome::esp32_ble_tracker { static const char *const TAG = "esp32_ble_tracker"; ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +const char *client_state_to_string(ClientState state) { + switch (state) { + case ClientState::INIT: + return "INIT"; + case ClientState::DISCONNECTING: + return "DISCONNECTING"; + case ClientState::IDLE: + return "IDLE"; + case ClientState::SEARCHING: + return "SEARCHING"; + case ClientState::DISCOVERED: + return "DISCOVERED"; + case ClientState::READY_TO_CONNECT: + return "READY_TO_CONNECT"; + case ClientState::CONNECTING: + return "CONNECTING"; + case ClientState::CONNECTED: + return "CONNECTED"; + case ClientState::ESTABLISHED: + return "ESTABLISHED"; + default: + return "UNKNOWN"; + } +} + float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } void ESP32BLETracker::setup() { @@ -50,13 +74,6 @@ void ESP32BLETracker::setup() { ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE"); return; } - RAMAllocator allocator; - this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE); - - if (this->scan_ring_buffer_ == nullptr) { - ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!"); - this->mark_failed(); - } global_esp32_ble_tracker = this; @@ -84,118 +101,49 @@ void ESP32BLETracker::loop() { this->start_scan(); } } - int connecting = 0; - int discovered = 0; - int searching = 0; - int disconnecting = 0; - for (auto *client : this->clients_) { - switch (client->state()) { - case ClientState::DISCONNECTING: - disconnecting++; - break; - case ClientState::DISCOVERED: - discovered++; - break; - case ClientState::SEARCHING: - searching++; - break; - case ClientState::CONNECTING: - case ClientState::READY_TO_CONNECT: - connecting++; - break; - default: - break; - } - } - if (connecting != connecting_ || discovered != discovered_ || searching != searching_ || - disconnecting != disconnecting_) { - connecting_ = connecting; - discovered_ = discovered; - searching_ = searching; - disconnecting_ = disconnecting; - ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_, - searching_, disconnecting_); - } - bool promote_to_connecting = discovered && !searching && !connecting; - // Process scan results from lock-free SPSC ring buffer - // Consumer side: This runs in the main loop thread + // Check for scan timeout - moved here from scheduler to avoid false reboots + // when the loop is blocked if (this->scanner_state_ == ScannerState::RUNNING) { - // Load our own index with relaxed ordering (we're the only writer) - uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed); - - // Load producer's index with acquire to see their latest writes - uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); - - while (read_idx != write_idx) { - // Process one result at a time directly from ring buffer - BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx]; - - if (this->raw_advertisements_) { - for (auto *listener : this->listeners_) { - listener->parse_devices(&scan_result, 1); - } - for (auto *client : this->clients_) { - client->parse_devices(&scan_result, 1); + switch (this->scan_timeout_state_) { + case ScanTimeoutState::MONITORING: { + uint32_t now = App.get_loop_component_start_time(); + uint32_t timeout_ms = this->scan_duration_ * 2000; + // Robust time comparison that handles rollover correctly + // This works because unsigned arithmetic wraps around predictably + if ((now - this->scan_start_time_) > timeout_ms) { + // First time we've seen the timeout exceeded - wait one more loop iteration + // This ensures all components have had a chance to process pending events + // This is because esp32_ble may not have run yet and called + // gap_scan_event_handler yet when the loop unblocks + ESP_LOGW(TAG, "Scan timeout exceeded"); + this->scan_timeout_state_ = ScanTimeoutState::EXCEEDED_WAIT; } + break; } + case ScanTimeoutState::EXCEEDED_WAIT: + // We've waited at least one full loop iteration, and scan is still running + ESP_LOGE(TAG, "Scan never terminated, rebooting"); + App.reboot(); + break; - if (this->parse_advertisements_) { - ESPBTDevice device; - device.parse_scan_rst(scan_result); - - bool found = false; - for (auto *listener : this->listeners_) { - if (listener->parse_device(device)) - found = true; - } - - for (auto *client : this->clients_) { - if (client->parse_device(device)) { - found = true; - if (!connecting && client->state() == ClientState::DISCOVERED) { - promote_to_connecting = true; - } - } - } - - if (!found && !this->scan_continuous_) { - this->print_bt_device_info(device); - } - } - - // Move to next entry in ring buffer - read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE; - - // Store with release to ensure reads complete before index update - this->ring_read_index_.store(read_idx, std::memory_order_release); - } - - // Log dropped results periodically - size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed); - if (dropped > 0) { - ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped); + case ScanTimeoutState::INACTIVE: + // This case should be unreachable - scanner and timeout states are always synchronized + break; } } - if (this->scanner_state_ == ScannerState::STOPPED) { - this->end_of_scan_(); // Change state to IDLE + + ClientStateCounts counts = this->count_client_states_(); + if (counts != this->client_state_counts_) { + this->client_state_counts_ = counts; + ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d", + this->client_state_counts_.connecting, this->client_state_counts_.discovered, + this->client_state_counts_.searching, this->client_state_counts_.disconnecting); } + if (this->scanner_state_ == ScannerState::FAILED || (this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) { - this->stop_scan_(); - if (this->scan_start_fail_count_ == std::numeric_limits::max()) { - ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)", - std::numeric_limits::max()); - App.reboot(); - } - if (this->scan_start_failed_) { - ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_); - this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS; - } - if (this->scan_set_param_failed_) { - ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_); - this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; - } + this->handle_scanner_failure_(); } /* @@ -210,13 +158,12 @@ void ESP32BLETracker::loop() { https://github.com/espressif/esp-idf/issues/6688 */ - if (this->scanner_state_ == ScannerState::IDLE && !connecting && !disconnecting && !promote_to_connecting) { + bool promote_to_connecting = counts.discovered && !counts.searching && !counts.connecting; + + if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && + !promote_to_connecting) { #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE - if (this->coex_prefer_ble_) { - this->coex_prefer_ble_ = false; - ESP_LOGD(TAG, "Setting coexistence preference to balanced."); - esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default - } + this->update_coex_preference_(false); #endif if (this->scan_continuous_) { this->start_scan_(false); // first = false @@ -224,31 +171,13 @@ void ESP32BLETracker::loop() { } // If there is a discovered client and no connecting // clients and no clients using the scanner to search for - // devices, then stop scanning and promote the discovered - // client to ready to connect. + // devices, then promote the discovered client to ready to connect. + // We check both RUNNING and IDLE states because: + // - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately + // - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler) if (promote_to_connecting && (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) { - for (auto *client : this->clients_) { - if (client->state() == ClientState::DISCOVERED) { - if (this->scanner_state_ == ScannerState::RUNNING) { - ESP_LOGD(TAG, "Stopping scan to make connection"); - this->stop_scan_(); - } else if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGD(TAG, "Promoting client to connect"); - // We only want to promote one client at a time. - // once the scanner is fully stopped. -#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE - ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection."); - if (!this->coex_prefer_ble_) { - this->coex_prefer_ble_ = true; - esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth - } -#endif - client->set_state(ClientState::READY_TO_CONNECT); - } - break; - } - } + this->try_promote_discovered_clients_(); } } @@ -264,18 +193,11 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); void ESP32BLETracker::stop_scan_() { if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) { - if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGE(TAG, "Scan is already stopped while trying to stop."); - } else if (this->scanner_state_ == ScannerState::STARTING) { - ESP_LOGE(TAG, "Scan is starting while trying to stop."); - } else if (this->scanner_state_ == ScannerState::STOPPING) { - ESP_LOGE(TAG, "Scan is already stopping while trying to stop."); - } else if (this->scanner_state_ == ScannerState::STOPPED) { - ESP_LOGE(TAG, "Scan is already stopped while trying to stop."); - } + ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_)); return; } - this->cancel_timeout("scan"); + // Reset timeout state machine when stopping scan + this->scan_timeout_state_ = ScanTimeoutState::INACTIVE; this->set_scanner_state_(ScannerState::STOPPING); esp_err_t err = esp_ble_gap_stop_scanning(); if (err != ESP_OK) { @@ -290,17 +212,7 @@ void ESP32BLETracker::start_scan_(bool first) { return; } if (this->scanner_state_ != ScannerState::IDLE) { - if (this->scanner_state_ == ScannerState::STARTING) { - ESP_LOGE(TAG, "Cannot start scan while already starting."); - } else if (this->scanner_state_ == ScannerState::RUNNING) { - ESP_LOGE(TAG, "Cannot start scan while already running."); - } else if (this->scanner_state_ == ScannerState::STOPPING) { - ESP_LOGE(TAG, "Cannot start scan while already stopping."); - } else if (this->scanner_state_ == ScannerState::FAILED) { - ESP_LOGE(TAG, "Cannot start scan while already failed."); - } else if (this->scanner_state_ == ScannerState::STOPPED) { - ESP_LOGE(TAG, "Cannot start scan while already stopped."); - } + this->log_unexpected_state_("start scan", ScannerState::IDLE); return; } this->set_scanner_state_(ScannerState::STARTING); @@ -309,18 +221,19 @@ void ESP32BLETracker::start_scan_(bool first) { for (auto *listener : this->listeners_) listener->on_scan_end(); } +#ifdef USE_ESP32_BLE_DEVICE this->already_discovered_.clear(); +#endif this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE; this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL; this->scan_params_.scan_interval = this->scan_interval_; this->scan_params_.scan_window = this->scan_window_; - // Start timeout before scan is started. Otherwise scan never starts if any error. - this->set_timeout("scan", this->scan_duration_ * 2000, []() { - ESP_LOGE(TAG, "Scan never terminated, rebooting to restore stack (IDF)"); - App.reboot(); - }); + // Start timeout monitoring in loop() instead of using scheduler + // This prevents false reboots when the loop is blocked + this->scan_start_time_ = App.get_loop_component_start_time(); + this->scan_timeout_state_ = ScanTimeoutState::MONITORING; esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_); if (err != ESP_OK) { @@ -334,21 +247,6 @@ void ESP32BLETracker::start_scan_(bool first) { } } -void ESP32BLETracker::end_of_scan_() { - // The lock must be held when calling this function. - if (this->scanner_state_ != ScannerState::STOPPED) { - ESP_LOGE(TAG, "end_of_scan_ called while scanner is not stopped."); - return; - } - ESP_LOGD(TAG, "End of scan, set scanner state to IDLE."); - this->already_discovered_.clear(); - this->cancel_timeout("scan"); - - for (auto *listener : this->listeners_) - listener->on_scan_end(); - this->set_scanner_state_(ScannerState::IDLE); -} - void ESP32BLETracker::register_client(ESPBTClient *client) { client->app_id = ++this->app_id_; this->clients_.push_back(client); @@ -381,6 +279,8 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() { } void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + // Note: This handler is called from the main loop context, not directly from the BT task. + // The esp32_ble component queues events via enqueue_ble_event() and processes them in loop(). switch (event) { case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: this->gap_scan_set_param_complete_(param->scan_param_cmpl); @@ -401,51 +301,32 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga } void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { + // Note: This handler is called from the main loop context via esp32_ble's event queue. + // We process advertisements immediately instead of buffering them. ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt); if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { - // Lock-free SPSC ring buffer write (Producer side) - // This runs in the ESP-IDF Bluetooth stack callback thread - // IMPORTANT: Only this thread writes to ring_write_index_ + // Process the scan result immediately + bool found_discovered_client = this->process_scan_result_(scan_result); - // Load our own index with relaxed ordering (we're the only writer) - uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed); - uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE; - - // Load consumer's index with acquire to see their latest updates - uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire); - - // Check if buffer is full - if (next_write_idx != read_idx) { - // Write to ring buffer - this->scan_ring_buffer_[write_idx] = scan_result; - - // Store with release to ensure the write is visible before index update - this->ring_write_index_.store(next_write_idx, std::memory_order_release); - } else { - // Buffer full, track dropped results - this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed); + // If we found a discovered client that needs promotion, stop scanning + // This replaces the promote_to_connecting logic from loop() + if (found_discovered_client && this->scanner_state_ == ScannerState::RUNNING) { + ESP_LOGD(TAG, "Found discovered client, stopping scan for connection"); + this->stop_scan_(); } } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { // Scan finished on its own if (this->scanner_state_ != ScannerState::RUNNING) { - if (this->scanner_state_ == ScannerState::STOPPING) { - ESP_LOGE(TAG, "Scan was not running when scan completed."); - } else if (this->scanner_state_ == ScannerState::STARTING) { - ESP_LOGE(TAG, "Scan was not started when scan completed."); - } else if (this->scanner_state_ == ScannerState::FAILED) { - ESP_LOGE(TAG, "Scan was in failed state when scan completed."); - } else if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGE(TAG, "Scan was idle when scan completed."); - } else if (this->scanner_state_ == ScannerState::STOPPED) { - ESP_LOGE(TAG, "Scan was stopped when scan completed."); - } + this->log_unexpected_state_("scan complete", ScannerState::RUNNING); } - this->set_scanner_state_(ScannerState::STOPPED); + // Scan completed naturally, perform cleanup and transition to IDLE + this->cleanup_scan_state_(false); } } void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) { + // Called from main loop context via gap_event_handler after being queued from BT task ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status); if (param.status == ESP_BT_STATUS_DONE) { this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; @@ -455,20 +336,11 @@ void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t: } void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) { + // Called from main loop context via gap_event_handler after being queued from BT task ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status); this->scan_start_failed_ = param.status; if (this->scanner_state_ != ScannerState::STARTING) { - if (this->scanner_state_ == ScannerState::RUNNING) { - ESP_LOGE(TAG, "Scan was already running when start complete."); - } else if (this->scanner_state_ == ScannerState::STOPPING) { - ESP_LOGE(TAG, "Scan was stopping when start complete."); - } else if (this->scanner_state_ == ScannerState::FAILED) { - ESP_LOGE(TAG, "Scan was in failed state when start complete."); - } else if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGE(TAG, "Scan was idle when start complete."); - } else if (this->scanner_state_ == ScannerState::STOPPED) { - ESP_LOGE(TAG, "Scan was stopped when start complete."); - } + this->log_unexpected_state_("start complete", ScannerState::STARTING); } if (param.status == ESP_BT_STATUS_SUCCESS) { this->scan_start_fail_count_ = 0; @@ -482,21 +354,15 @@ void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble } void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) { + // Called from main loop context via gap_event_handler after being queued from BT task + // This allows us to safely transition to IDLE state and perform cleanup without race conditions ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status); if (this->scanner_state_ != ScannerState::STOPPING) { - if (this->scanner_state_ == ScannerState::RUNNING) { - ESP_LOGE(TAG, "Scan was not running when stop complete."); - } else if (this->scanner_state_ == ScannerState::STARTING) { - ESP_LOGE(TAG, "Scan was not started when stop complete."); - } else if (this->scanner_state_ == ScannerState::FAILED) { - ESP_LOGE(TAG, "Scan was in failed state when stop complete."); - } else if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGE(TAG, "Scan was idle when stop complete."); - } else if (this->scanner_state_ == ScannerState::STOPPED) { - ESP_LOGE(TAG, "Scan was stopped when stop complete."); - } + this->log_unexpected_state_("stop complete", ScannerState::STOPPING); } - this->set_scanner_state_(ScannerState::STOPPED); + + // Perform cleanup and transition to IDLE + this->cleanup_scan_state_(true); } void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, @@ -511,6 +377,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 +618,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:"); @@ -769,33 +639,16 @@ void ESP32BLETracker::dump_config() { " Continuous Scanning: %s", this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f, this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_)); - switch (this->scanner_state_) { - case ScannerState::IDLE: - ESP_LOGCONFIG(TAG, " Scanner State: IDLE"); - break; - case ScannerState::STARTING: - ESP_LOGCONFIG(TAG, " Scanner State: STARTING"); - break; - case ScannerState::RUNNING: - ESP_LOGCONFIG(TAG, " Scanner State: RUNNING"); - break; - case ScannerState::STOPPING: - ESP_LOGCONFIG(TAG, " Scanner State: STOPPING"); - break; - case ScannerState::STOPPED: - ESP_LOGCONFIG(TAG, " Scanner State: STOPPED"); - break; - case ScannerState::FAILED: - ESP_LOGCONFIG(TAG, " Scanner State: FAILED"); - break; - } - ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_, - searching_, disconnecting_); + ESP_LOGCONFIG(TAG, " Scanner State: %s", this->scanner_state_to_string_(this->scanner_state_)); + ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", + this->client_state_counts_.connecting, this->client_state_counts_.discovered, + this->client_state_counts_.searching, this->client_state_counts_.disconnecting); if (this->scan_start_fail_count_) { ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_); } } +#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_) { @@ -867,7 +720,157 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); } -} // namespace esp32_ble_tracker -} // namespace esphome +bool ESP32BLETracker::has_connecting_clients_() const { + for (auto *client : this->clients_) { + auto state = client->state(); + if (state == ClientState::CONNECTING || state == ClientState::READY_TO_CONNECT) { + return true; + } + } + return false; +} +#endif // USE_ESP32_BLE_DEVICE +bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { + bool found_discovered_client = false; + + // Process raw advertisements + if (this->raw_advertisements_) { + for (auto *listener : this->listeners_) { + listener->parse_devices(&scan_result, 1); + } + for (auto *client : this->clients_) { + client->parse_devices(&scan_result, 1); + } + } + + // Process parsed advertisements + if (this->parse_advertisements_) { +#ifdef USE_ESP32_BLE_DEVICE + ESPBTDevice device; + device.parse_scan_rst(scan_result); + + bool found = false; + for (auto *listener : this->listeners_) { + if (listener->parse_device(device)) + found = true; + } + + for (auto *client : this->clients_) { + if (client->parse_device(device)) { + found = true; + // Check if this client is discovered and needs promotion + if (client->state() == ClientState::DISCOVERED) { + // Only check for connecting clients if we found a discovered client + // This matches the original logic: !connecting && client->state() == DISCOVERED + if (!this->has_connecting_clients_()) { + found_discovered_client = true; + } + } + } + } + + if (!found && !this->scan_continuous_) { + this->print_bt_device_info(device); + } +#endif // USE_ESP32_BLE_DEVICE + } + + return found_discovered_client; +} + +void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { + ESP_LOGD(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : ""); +#ifdef USE_ESP32_BLE_DEVICE + this->already_discovered_.clear(); #endif + // Reset timeout state machine instead of cancelling scheduler timeout + this->scan_timeout_state_ = ScanTimeoutState::INACTIVE; + + for (auto *listener : this->listeners_) + listener->on_scan_end(); + + this->set_scanner_state_(ScannerState::IDLE); +} + +void ESP32BLETracker::handle_scanner_failure_() { + this->stop_scan_(); + if (this->scan_start_fail_count_ == std::numeric_limits::max()) { + ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)", + std::numeric_limits::max()); + App.reboot(); + } + if (this->scan_start_failed_) { + ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_); + this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS; + } + if (this->scan_set_param_failed_) { + ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_); + this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; + } +} + +void ESP32BLETracker::try_promote_discovered_clients_() { + // Only promote the first discovered client to avoid multiple simultaneous connections + for (auto *client : this->clients_) { + if (client->state() != ClientState::DISCOVERED) { + continue; + } + + if (this->scanner_state_ == ScannerState::RUNNING) { + ESP_LOGD(TAG, "Stopping scan to make connection"); + this->stop_scan_(); + // Don't wait for scan stop complete - promote immediately. + // This is safe because ESP-IDF processes BLE commands sequentially through its internal mailbox queue. + // This guarantees that the stop scan command will be fully processed before any subsequent connect command, + // preventing race conditions or overlapping operations. + } + + ESP_LOGD(TAG, "Promoting client to connect"); +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE + this->update_coex_preference_(true); +#endif + client->set_state(ClientState::READY_TO_CONNECT); + break; + } +} + +const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const { + switch (state) { + case ScannerState::IDLE: + return "IDLE"; + case ScannerState::STARTING: + return "STARTING"; + case ScannerState::RUNNING: + return "RUNNING"; + case ScannerState::STOPPING: + return "STOPPING"; + case ScannerState::FAILED: + return "FAILED"; + default: + return "UNKNOWN"; + } +} + +void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState expected_state) const { + ESP_LOGE(TAG, "Unexpected state: %s on %s, expected: %s", this->scanner_state_to_string_(this->scanner_state_), + operation, this->scanner_state_to_string_(expected_state)); +} + +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE +void ESP32BLETracker::update_coex_preference_(bool force_ble) { + if (force_ble && !this->coex_prefer_ble_) { + ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection."); + this->coex_prefer_ble_ = true; + esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth + } else if (!force_ble && this->coex_prefer_ble_) { + ESP_LOGD(TAG, "Setting coexistence preference to balanced."); + this->coex_prefer_ble_ = false; + esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default + } +} +#endif + +} // namespace esphome::esp32_ble_tracker + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index f5ed75a93e..3022eb25d2 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -6,7 +6,6 @@ #include "esphome/core/helpers.h" #include -#include #include #include @@ -21,9 +20,9 @@ #include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble/ble_uuid.h" +#include "esphome/components/esp32_ble/ble_scan_result.h" -namespace esphome { -namespace esp32_ble_tracker { +namespace esphome::esp32_ble_tracker { using namespace esp32_ble; @@ -34,11 +33,14 @@ enum AdvertisementParserType { RAW_ADVERTISEMENTS, }; +#ifdef USE_ESP32_BLE_UUID struct ServiceData { ESPBTUUID uuid; adv_data_t data; }; +#endif +#ifdef USE_ESP32_BLE_DEVICE class ESPBLEiBeacon { public: ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); } @@ -116,13 +118,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; @@ -133,6 +138,20 @@ class ESPBTDeviceListener { ESP32BLETracker *parent_{nullptr}; }; +struct ClientStateCounts { + uint8_t connecting = 0; + uint8_t discovered = 0; + uint8_t searching = 0; + uint8_t disconnecting = 0; + + bool operator==(const ClientStateCounts &other) const { + return connecting == other.connecting && discovered == other.discovered && searching == other.searching && + disconnecting == other.disconnecting; + } + + bool operator!=(const ClientStateCounts &other) const { return !(*this == other); } +}; + enum class ClientState : uint8_t { // Connection is allocated INIT, @@ -155,20 +174,21 @@ enum class ClientState : uint8_t { }; enum class ScannerState { - // Scanner is idle, init state, set from the main loop when processing STOPPED + // Scanner is idle, init state IDLE, - // Scanner is starting, set from the main loop only + // Scanner is starting STARTING, - // Scanner is running, set from the ESP callback only + // Scanner is running RUNNING, - // Scanner failed to start, set from the ESP callback only + // Scanner failed to start FAILED, - // Scanner is stopping, set from the main loop only + // Scanner is stopping STOPPING, - // Scanner is stopped, set from the ESP callback only - STOPPED, }; +// Helper function to convert ClientState to string +const char *client_state_to_string(ClientState state); + enum class ConnectionType : uint8_t { // The default connection type, we hold all the services in ram // for the duration of the connection. @@ -237,7 +257,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(); @@ -257,8 +279,6 @@ class ESP32BLETracker : public Component, void stop_scan_(); /// Start a single scan by setting up the parameters and doing some esp-idf calls. void start_scan_(bool first); - /// Called when a scan ends - void end_of_scan_(); /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received. @@ -269,53 +289,99 @@ class ESP32BLETracker : public Component, void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); /// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed. void set_scanner_state_(ScannerState state); + /// Common cleanup logic when transitioning scanner to IDLE state + void cleanup_scan_state_(bool is_stop_complete); + /// Process a single scan result immediately + /// Returns true if a discovered client needs promotion to READY_TO_CONNECT + bool process_scan_result_(const BLEScanResult &scan_result); +#ifdef USE_ESP32_BLE_DEVICE + /// Check if any clients are in connecting or ready to connect state + bool has_connecting_clients_() const; +#endif + /// Handle scanner failure states + void handle_scanner_failure_(); + /// Try to promote discovered clients to ready to connect + void try_promote_discovered_clients_(); + /// Convert scanner state enum to string for logging + const char *scanner_state_to_string_(ScannerState state) const; + /// Log an unexpected scanner state + void log_unexpected_state_(const char *operation, ScannerState expected_state) const; +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE + /// Update BLE coexistence preference + void update_coex_preference_(bool force_ble); +#endif + /// Count clients in each state + ClientStateCounts count_client_states_() const { + ClientStateCounts counts; + for (auto *client : this->clients_) { + switch (client->state()) { + case ClientState::DISCONNECTING: + counts.disconnecting++; + break; + case ClientState::DISCOVERED: + counts.discovered++; + break; + case ClientState::SEARCHING: + counts.searching++; + break; + case ClientState::CONNECTING: + case ClientState::READY_TO_CONNECT: + counts.connecting++; + break; + default: + break; + } + } + return counts; + } - uint8_t app_id_{0}; - + // Group 1: Large objects (12+ bytes) - vectors and callback manager + std::vector listeners_; + std::vector clients_; + CallbackManager scanner_state_callbacks_; +#ifdef USE_ESP32_BLE_DEVICE /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; - std::vector listeners_; - /// Client parameters. - std::vector clients_; +#endif + + // Group 2: Structs (aligned to 4 bytes) /// A structure holding the ESP BLE scan parameters. esp_ble_scan_params_t scan_params_; + ClientStateCounts client_state_counts_; + + // Group 3: 4-byte types /// The interval in seconds to perform scans. uint32_t scan_duration_; uint32_t scan_interval_; uint32_t scan_window_; + esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; + esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; + + // Group 4: 1-byte types (enums, uint8_t, bool) + uint8_t app_id_{0}; uint8_t scan_start_fail_count_{0}; + ScannerState scanner_state_{ScannerState::IDLE}; bool scan_continuous_; bool scan_active_; - ScannerState scanner_state_{ScannerState::IDLE}; - CallbackManager scanner_state_callbacks_; bool ble_was_disabled_{true}; bool raw_advertisements_{false}; bool parse_advertisements_{false}; - - // Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results - // Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler) - // Consumer: ESPHome main loop (loop() method) - // This design ensures zero blocking in the BT callback and prevents scan result loss - BLEScanResult *scan_ring_buffer_; - std::atomic ring_write_index_{0}; // Written only by BT callback (producer) - std::atomic ring_read_index_{0}; // Written only by main loop (consumer) - std::atomic scan_results_dropped_{0}; // Tracks buffer overflow events - - esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; - esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; - int connecting_{0}; - int discovered_{0}; - int searching_{0}; - int disconnecting_{0}; #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE bool coex_prefer_ble_{false}; #endif + // Scan timeout state machine + enum class ScanTimeoutState : uint8_t { + INACTIVE, // No timeout monitoring + MONITORING, // Actively monitoring for timeout + EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot + }; + uint32_t scan_start_time_{0}; + ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE}; }; // NOLINTNEXTLINE extern ESP32BLETracker *global_esp32_ble_tracker; -} // namespace esp32_ble_tracker -} // namespace esphome +} // namespace esphome::esp32_ble_tracker #endif diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index a99ec34087..6206fe4682 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -119,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" @@ -149,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" @@ -230,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( @@ -301,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", } @@ -328,12 +339,13 @@ async def to_code(config): else: cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE])) cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT])) + cg.add(var.set_frame_buffer_location(config[CONF_FRAME_BUFFER_LOCATION])) cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add_define("USE_CAMERA") if CORE.using_esp_idf: - add_idf_component(name="espressif/esp32-camera", ref="2.0.15") + add_idf_component(name="espressif/esp32-camera", ref="2.1.1") for conf in config.get(CONF_ON_STREAM_START, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/esp32_camera/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_dac/esp32_dac.cpp b/esphome/components/esp32_dac/esp32_dac.cpp index 01bf0e04c3..8f226a5cc2 100644 --- a/esphome/components/esp32_dac/esp32_dac.cpp +++ b/esphome/components/esp32_dac/esp32_dac.cpp @@ -2,11 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#ifdef USE_ESP32 - -#ifdef USE_ARDUINO -#include -#endif +#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2) namespace esphome { namespace esp32_dac { @@ -20,22 +16,15 @@ static constexpr uint8_t DAC0_PIN = 25; static const char *const TAG = "esp32_dac"; void ESP32DAC::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->pin_->setup(); this->turn_off(); -#ifdef USE_ESP_IDF const dac_channel_t channel = this->pin_->get_pin() == DAC0_PIN ? DAC_CHAN_0 : DAC_CHAN_1; const dac_oneshot_config_t oneshot_cfg{channel}; dac_oneshot_new_channel(&oneshot_cfg, &this->dac_handle_); -#endif } -void ESP32DAC::on_safe_shutdown() { -#ifdef USE_ESP_IDF - dac_oneshot_del_channel(this->dac_handle_); -#endif -} +void ESP32DAC::on_safe_shutdown() { dac_oneshot_del_channel(this->dac_handle_); } void ESP32DAC::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 DAC:"); @@ -49,15 +38,10 @@ void ESP32DAC::write_state(float state) { state = state * 255; -#ifdef USE_ESP_IDF dac_oneshot_output_voltage(this->dac_handle_, state); -#endif -#ifdef USE_ARDUINO - dacWrite(this->pin_->get_pin(), state); -#endif } } // namespace esp32_dac } // namespace esphome -#endif +#endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2 diff --git a/esphome/components/esp32_dac/esp32_dac.h b/esphome/components/esp32_dac/esp32_dac.h index 63d0c914a1..95c687d307 100644 --- a/esphome/components/esp32_dac/esp32_dac.h +++ b/esphome/components/esp32_dac/esp32_dac.h @@ -1,15 +1,13 @@ #pragma once +#include "esphome/components/output/float_output.h" +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" -#include "esphome/core/automation.h" -#include "esphome/components/output/float_output.h" -#ifdef USE_ESP32 +#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2) -#ifdef USE_ESP_IDF #include -#endif namespace esphome { namespace esp32_dac { @@ -29,12 +27,10 @@ class ESP32DAC : public output::FloatOutput, public Component { void write_state(float state) override; InternalGPIOPin *pin_; -#ifdef USE_ESP_IDF dac_oneshot_handle_t dac_handle_; -#endif }; } // namespace esp32_dac } // namespace esphome -#endif +#endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2 diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 389c32882b..344ea35e81 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -42,9 +42,6 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size symbols[i] = params->bit0; } } - if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) { - *done = true; - } return RMT_SYMBOLS_PER_BYTE; } @@ -59,8 +56,6 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size #endif void ESP32RMTLEDStripLightOutput::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - size_t buffer_size = this->get_buffer_size_(); RAMAllocator allocator(this->use_psram_ ? 0 : RAMAllocator::ALLOC_INTERNAL); @@ -112,7 +107,7 @@ void ESP32RMTLEDStripLightOutput::setup() { memset(&encoder, 0, sizeof(encoder)); encoder.callback = encoder_callback; encoder.arg = &this->params_; - encoder.min_chunk_size = 8; + encoder.min_chunk_size = RMT_SYMBOLS_PER_BYTE; if (rmt_new_simple_encoder(&encoder, &this->encoder_) != ESP_OK) { ESP_LOGE(TAG, "Encoder creation failed"); this->mark_failed(); diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 33ae44e435..ac4f0b2e92 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -4,6 +4,7 @@ import logging from esphome import pins import esphome.codegen as cg from esphome.components import esp32, light +from esphome.components.const import CONF_USE_PSRAM import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, @@ -57,7 +58,6 @@ CHIPSETS = { "SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0), } -CONF_USE_PSRAM = "use_psram" CONF_IS_WRGB = "is_wrgb" CONF_BIT0_HIGH = "bit0_high" CONF_BIT0_LOW = "bit0_low" diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index 224e301683..b6cb19ebb1 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -294,9 +294,8 @@ async def to_code(config): ) ) - if get_esp32_variant() == VARIANT_ESP32: - if CONF_IIR_FILTER in config: - cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER])) + if get_esp32_variant() == VARIANT_ESP32 and CONF_IIR_FILTER in config: + cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER])) if get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3: if CONF_FILTER_MODE in config: diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 5a91b1c750..fb1973e26f 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -171,8 +171,8 @@ class ESP32TouchComponent : public Component { // based on the filter configuration uint32_t read_touch_value(touch_pad_t pad) const; - // Helper to update touch state with a known state - void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched); + // Helper to update touch state with a known state and value + void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value); // Helper to read touch value and update state for a given child bool check_and_update_touch_state_(ESP32TouchBinarySensor *child); @@ -234,9 +234,13 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { touch_pad_t get_touch_pad() const { return this->touch_pad_; } uint32_t get_threshold() const { return this->threshold_; } void set_threshold(uint32_t threshold) { this->threshold_ = threshold; } -#ifdef USE_ESP32_VARIANT_ESP32 + + /// Get the raw touch measurement value. + /// @note Although this method may appear unused within the component, it is a public API + /// used by lambdas in user configurations for custom touch value processing. + /// @return The current raw touch sensor reading uint32_t get_value() const { return this->value_; } -#endif + uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; } protected: @@ -245,9 +249,8 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { touch_pad_t touch_pad_{TOUCH_PAD_MAX}; uint32_t threshold_{0}; uint32_t benchmark_{}; -#ifdef USE_ESP32_VARIANT_ESP32 + /// Stores the last raw touch measurement value. uint32_t value_{0}; -#endif bool last_state_{false}; const uint32_t wakeup_threshold_{0}; diff --git a/esphome/components/esp32_touch/esp32_touch_common.cpp b/esphome/components/esp32_touch/esp32_touch_common.cpp index 2d93de077e..a0b1df38c1 100644 --- a/esphome/components/esp32_touch/esp32_touch_common.cpp +++ b/esphome/components/esp32_touch/esp32_touch_common.cpp @@ -100,6 +100,8 @@ void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) { #else // Read the value being used for touch detection uint32_t value = this->read_touch_value(child->get_touch_pad()); + // Store the value for get_value() access in lambdas + child->value_ = value; ESP_LOGD(TAG, "Touch Pad '%s' (T%d): %d", child->get_name().c_str(), child->get_touch_pad(), value); #endif } diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp index 629dc8e793..ffb805e008 100644 --- a/esphome/components/esp32_touch/esp32_touch_v1.cpp +++ b/esphome/components/esp32_touch/esp32_touch_v1.cpp @@ -201,15 +201,13 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { touch_pad_t pad = child->get_touch_pad(); // Read current value using ISR-safe API - uint32_t value; - if (component->iir_filter_enabled_()) { - uint16_t temp_value = 0; - touch_pad_read_filtered(pad, &temp_value); - value = temp_value; - } else { - // Use low-level HAL function when filter is not enabled - value = touch_ll_read_raw_data(pad); - } + // IMPORTANT: ESP-IDF v5.4 regression - touch_pad_read_filtered() is no longer ISR-safe + // In ESP-IDF v5.3 and earlier it was ISR-safe, but ESP-IDF v5.4 added mutex protection that causes: + // "assert failed: xQueueSemaphoreTake queue.c:1718" + // We must use raw values even when filter is enabled as a workaround. + // Users should adjust thresholds to compensate for the lack of IIR filtering. + // See: https://github.com/espressif/esp-idf/issues/17045 + uint32_t value = touch_ll_read_raw_data(pad); // Skip pads that aren’t in the trigger mask if (((mask >> pad) & 1) == 0) { diff --git a/esphome/components/esp32_touch/esp32_touch_v2.cpp b/esphome/components/esp32_touch/esp32_touch_v2.cpp index afd2655fd7..9662b009f6 100644 --- a/esphome/components/esp32_touch/esp32_touch_v2.cpp +++ b/esphome/components/esp32_touch/esp32_touch_v2.cpp @@ -10,8 +10,11 @@ namespace esp32_touch { static const char *const TAG = "esp32_touch"; -// Helper to update touch state with a known state -void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched) { +// Helper to update touch state with a known state and value +void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value) { + // Store the value for get_value() access in lambdas + child->value_ = value; + // Always update timer when touched if (is_touched) { child->last_touch_time_ = App.get_loop_component_start_time(); @@ -21,9 +24,8 @@ void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, boo child->last_state_ = is_touched; child->publish_state(is_touched); if (is_touched) { - // ESP32-S2/S3 v2: touched when value > threshold ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(), - this->read_touch_value(child->touch_pad_), child->threshold_ + child->benchmark_); + value, child->threshold_ + child->benchmark_); } else { ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str()); } @@ -41,7 +43,7 @@ bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor * child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_); bool is_touched = value > child->benchmark_ + child->threshold_; - this->update_touch_state_(child, is_touched); + this->update_touch_state_(child, is_touched, value); return is_touched; } @@ -296,7 +298,9 @@ void ESP32TouchComponent::loop() { this->check_and_update_touch_state_(child); } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) { // We only get ACTIVE interrupts now, releases are detected by timeout - this->update_touch_state_(child, true); // Always touched for ACTIVE interrupts + // Read the current value + uint32_t value = this->read_touch_value(child->touch_pad_); + this->update_touch_state_(child, true, value); // Always touched for ACTIVE interrupts } break; } diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 01b20bdcb1..33a4149571 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, + ThreadModel, ) from esphome.core import CORE, coroutine_with_priority from esphome.helpers import copy_file_if_changed @@ -187,6 +188,7 @@ async def to_code(config): cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "ESP8266") + cg.add_define(ThreadModel.SINGLE) cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) @@ -243,7 +245,7 @@ async def to_code(config): if ver <= cv.Version(2, 3, 0): # No ld script support ld_script = None - if ver <= cv.Version(2, 4, 2): + elif ver <= cv.Version(2, 4, 2): # Old ld script path ld_script = ld_scripts[0] else: diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.cpp b/esphome/components/esp8266_pwm/esp8266_pwm.cpp index 03fa3c683e..0aaef597d3 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.cpp +++ b/esphome/components/esp8266_pwm/esp8266_pwm.cpp @@ -14,7 +14,6 @@ namespace esp8266_pwm { static const char *const TAG = "esp8266_pwm"; void ESP8266PWM::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->pin_->setup(); this->turn_off(); } diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 901657ec82..9facdc3bc6 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -73,8 +73,7 @@ def ota_esphome_final_validate(config): else: new_ota_conf.append(ota_conf) - for port_conf in merged_ota_esphome_configs_by_port.values(): - new_ota_conf.append(port_conf) + new_ota_conf.extend(merged_ota_esphome_configs_by_port.values()) full_conf[CONF_OTA] = new_ota_conf fv.full_config.set(full_conf) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 4cc82b9094..5217e9c61f 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -19,7 +19,9 @@ namespace esphome { static const char *const TAG = "esphome.ota"; -static constexpr u_int16_t OTA_BLOCK_SIZE = 8192; +static constexpr uint16_t OTA_BLOCK_SIZE = 8192; +static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake +static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer void ESPHomeOTAComponent::setup() { #ifdef USE_OTA_STATE_CALLBACK @@ -28,19 +30,19 @@ void ESPHomeOTAComponent::setup() { this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections if (this->server_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket"); + this->log_socket_error_("creation"); this->mark_failed(); return; } int enable = 1; int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + this->log_socket_error_("reuseaddr"); // we can still continue } err = this->server_->setblocking(false); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); + this->log_socket_error_("non-blocking"); this->mark_failed(); return; } @@ -49,21 +51,21 @@ void ESPHomeOTAComponent::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); + this->log_socket_error_("set sockaddr"); this->mark_failed(); return; } err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); + this->log_socket_error_("bind"); this->mark_failed(); return; } err = this->server_->listen(4); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); + this->log_socket_error_("listen"); this->mark_failed(); return; } @@ -83,17 +85,93 @@ void ESPHomeOTAComponent::dump_config() { } void ESPHomeOTAComponent::loop() { - // Skip handle_() call if no client connected and no incoming connections + // Skip handle_handshake_() call if no client connected and no incoming connections // This optimization reduces idle loop overhead when OTA is not active - // Note: No need to check server_ for null as the component is marked failed in setup() if server_ creation fails + // Note: No need to check server_ for null as the component is marked failed in setup() + // if server_ creation fails if (this->client_ != nullptr || this->server_->ready()) { - this->handle_(); + this->handle_handshake_(); } } static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; -void ESPHomeOTAComponent::handle_() { +void ESPHomeOTAComponent::handle_handshake_() { + /// Handle the initial OTA handshake. + /// + /// This method is non-blocking and will return immediately if no data is available. + /// It waits for the first magic byte (0x6C) before proceeding to handle_data_(). + /// A 10-second timeout is enforced from initial connection. + + if (this->client_ == nullptr) { + // We already checked server_->ready() in loop(), so we can accept directly + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + int enable = 1; + + this->client_ = this->server_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); + if (this->client_ == nullptr) + return; + int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); + if (err != 0) { + this->log_socket_error_("nodelay"); + this->cleanup_connection_(); + return; + } + err = this->client_->setblocking(false); + if (err != 0) { + this->log_socket_error_("non-blocking"); + this->cleanup_connection_(); + return; + } + this->log_start_("handshake"); + this->client_connect_time_ = App.get_loop_component_start_time(); + } + + // Check for handshake timeout + uint32_t now = App.get_loop_component_start_time(); + if (now - this->client_connect_time_ > OTA_SOCKET_TIMEOUT_HANDSHAKE) { + ESP_LOGW(TAG, "Handshake timeout"); + this->cleanup_connection_(); + return; + } + + // Try to read first byte of magic bytes + uint8_t first_byte; + ssize_t read = this->client_->read(&first_byte, 1); + + if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + return; // No data yet, try again next loop + } + + if (read <= 0) { + // Error or connection closed + if (read == -1) { + this->log_socket_error_("reading first byte"); + } else { + ESP_LOGW(TAG, "Remote closed during handshake"); + } + this->cleanup_connection_(); + return; + } + + // Got first byte, check if it's the magic byte + if (first_byte != 0x6C) { + ESP_LOGW(TAG, "Invalid initial byte: 0x%02X", first_byte); + this->cleanup_connection_(); + return; + } + + // First byte is valid, continue with data handling + this->handle_data_(); +} + +void ESPHomeOTAComponent::handle_data_() { + /// Handle the OTA data transfer and update process. + /// + /// This method is blocking and will not return until the OTA update completes, + /// fails, or times out. It handles authentication, receives the firmware data, + /// writes it to flash, and reboots on success. ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; bool update_started = false; size_t total = 0; @@ -108,38 +186,14 @@ void ESPHomeOTAComponent::handle_() { size_t size_acknowledged = 0; #endif - if (this->client_ == nullptr) { - // We already checked server_->ready() in loop(), so we can accept directly - struct sockaddr_storage source_addr; - socklen_t addr_len = sizeof(source_addr); - this->client_ = this->server_->accept((struct sockaddr *) &source_addr, &addr_len); - if (this->client_ == nullptr) - return; - } - - int enable = 1; - int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); - if (err != 0) { - ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno); - this->client_->close(); - this->client_ = nullptr; - return; - } - - ESP_LOGD(TAG, "Starting update from %s", this->client_->getpeername().c_str()); - this->status_set_warning(); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); -#endif - - if (!this->readall_(buf, 5)) { - ESP_LOGW(TAG, "Reading magic bytes failed"); + // Read remaining 4 bytes of magic (we already read the first byte 0x6C in handle_handshake_) + if (!this->readall_(buf, 4)) { + this->log_read_error_("magic bytes"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } - // 0x6C, 0x26, 0xF7, 0x5C, 0x45 - if (buf[0] != 0x6C || buf[1] != 0x26 || buf[2] != 0xF7 || buf[3] != 0x5C || buf[4] != 0x45) { - ESP_LOGW(TAG, "Magic bytes do not match! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3], - buf[4]); + // Check remaining magic bytes: 0x26, 0xF7, 0x5C, 0x45 + if (buf[0] != 0x26 || buf[1] != 0xF7 || buf[2] != 0x5C || buf[3] != 0x45) { + ESP_LOGW(TAG, "Magic bytes mismatch! 0x6C-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3]); error_code = ota::OTA_RESPONSE_ERROR_MAGIC; goto error; // NOLINT(cppcoreguidelines-avoid-goto) } @@ -153,7 +207,7 @@ void ESPHomeOTAComponent::handle_() { // Read features - 1 byte if (!this->readall_(buf, 1)) { - ESP_LOGW(TAG, "Reading features failed"); + this->log_read_error_("features"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } ota_features = buf[0]; // NOLINT @@ -232,7 +286,7 @@ void ESPHomeOTAComponent::handle_() { // Read size, 4 bytes MSB first if (!this->readall_(buf, 4)) { - ESP_LOGW(TAG, "Reading size failed"); + this->log_read_error_("size"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } ota_size = 0; @@ -242,6 +296,17 @@ void ESPHomeOTAComponent::handle_() { } ESP_LOGV(TAG, "Size is %u bytes", ota_size); + // Now that we've passed authentication and are actually + // starting the update, set the warning status and notify + // listeners. This ensures that port scanners do not + // accidentally trigger the update process. + this->log_start_("update"); + this->status_set_warning(); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); +#endif + + // This will block for a few seconds as it locks flash error_code = backend->begin(ota_size); if (error_code != ota::OTA_RESPONSE_OK) goto error; // NOLINT(cppcoreguidelines-avoid-goto) @@ -253,7 +318,7 @@ void ESPHomeOTAComponent::handle_() { // Read binary MD5, 32 bytes if (!this->readall_(buf, 32)) { - ESP_LOGW(TAG, "Reading binary MD5 checksum failed"); + this->log_read_error_("MD5 checksum"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } sbuf[32] = '\0'; @@ -270,23 +335,22 @@ void ESPHomeOTAComponent::handle_() { ssize_t read = this->client_->read(buf, requested); if (read == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { - App.feed_wdt(); - delay(1); + this->yield_and_feed_watchdog_(); continue; } - ESP_LOGW(TAG, "Error receiving data for update, errno %d", errno); + ESP_LOGW(TAG, "Read error, errno %d", errno); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } else if (read == 0) { // $ man recv // "When a stream socket peer has performed an orderly shutdown, the return value will // be 0 (the traditional "end-of-file" return)." - ESP_LOGW(TAG, "Remote end closed connection"); + ESP_LOGW(TAG, "Remote closed connection"); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } error_code = backend->write(buf, read); if (error_code != ota::OTA_RESPONSE_OK) { - ESP_LOGW(TAG, "Error writing binary data to flash!, error_code: %d", error_code); + ESP_LOGW(TAG, "Flash write error, code: %d", error_code); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } total += read; @@ -307,8 +371,7 @@ void ESPHomeOTAComponent::handle_() { this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0); #endif // feed watchdog and give other tasks a chance to run - App.feed_wdt(); - yield(); + this->yield_and_feed_watchdog_(); } } @@ -318,7 +381,7 @@ void ESPHomeOTAComponent::handle_() { error_code = backend->end(); if (error_code != ota::OTA_RESPONSE_OK) { - ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code); + ESP_LOGW(TAG, "Error ending update! code: %d", error_code); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } @@ -328,12 +391,11 @@ void ESPHomeOTAComponent::handle_() { // Read ACK if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { - ESP_LOGW(TAG, "Reading back acknowledgement failed"); + this->log_read_error_("ack"); // do not go to error, this is not fatal } - this->client_->close(); - this->client_ = nullptr; + this->cleanup_connection_(); delay(10); ESP_LOGI(TAG, "Update complete"); this->status_clear_warning(); @@ -346,8 +408,7 @@ void ESPHomeOTAComponent::handle_() { error: buf[0] = static_cast(error_code); this->writeall_(buf, 1); - this->client_->close(); - this->client_ = nullptr; + this->cleanup_connection_(); if (backend != nullptr && update_started) { backend->abort(); @@ -364,28 +425,24 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { uint32_t at = 0; while (len - at > 0) { uint32_t now = millis(); - if (now - start > 1000) { - ESP_LOGW(TAG, "Timed out reading %d bytes of data", len); + if (now - start > OTA_SOCKET_TIMEOUT_DATA) { + ESP_LOGW(TAG, "Timeout reading %d bytes", len); return false; } ssize_t read = this->client_->read(buf + at, len - at); if (read == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - App.feed_wdt(); - delay(1); - continue; + if (errno != EAGAIN && errno != EWOULDBLOCK) { + ESP_LOGW(TAG, "Error reading %d bytes, errno %d", len, errno); + return false; } - ESP_LOGW(TAG, "Failed to read %d bytes of data, errno %d", len, errno); - return false; } else if (read == 0) { ESP_LOGW(TAG, "Remote closed connection"); return false; } else { at += read; } - App.feed_wdt(); - delay(1); + this->yield_and_feed_watchdog_(); } return true; @@ -395,25 +452,21 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { uint32_t at = 0; while (len - at > 0) { uint32_t now = millis(); - if (now - start > 1000) { - ESP_LOGW(TAG, "Timed out writing %d bytes of data", len); + if (now - start > OTA_SOCKET_TIMEOUT_DATA) { + ESP_LOGW(TAG, "Timeout writing %d bytes", len); return false; } ssize_t written = this->client_->write(buf + at, len - at); if (written == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - App.feed_wdt(); - delay(1); - continue; + if (errno != EAGAIN && errno != EWOULDBLOCK) { + ESP_LOGW(TAG, "Error writing %d bytes, errno %d", len, errno); + return false; } - ESP_LOGW(TAG, "Failed to write %d bytes of data, errno %d", len, errno); - return false; } else { at += written; } - App.feed_wdt(); - delay(1); + this->yield_and_feed_watchdog_(); } return true; } @@ -421,5 +474,25 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; } + +void ESPHomeOTAComponent::log_socket_error_(const char *msg) { ESP_LOGW(TAG, "Socket %s: errno %d", msg, errno); } + +void ESPHomeOTAComponent::log_read_error_(const char *what) { ESP_LOGW(TAG, "Read %s failed", what); } + +void ESPHomeOTAComponent::log_start_(const char *phase) { + ESP_LOGD(TAG, "Starting %s from %s", phase, this->client_->getpeername().c_str()); +} + +void ESPHomeOTAComponent::cleanup_connection_() { + this->client_->close(); + this->client_ = nullptr; + this->client_connect_time_ = 0; +} + +void ESPHomeOTAComponent::yield_and_feed_watchdog_() { + App.feed_wdt(); + delay(1); +} + } // namespace esphome #endif diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index e0d09ff37e..8397b86528 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -27,15 +27,22 @@ class ESPHomeOTAComponent : public ota::OTAComponent { uint16_t get_port() const; protected: - void handle_(); + void handle_handshake_(); + void handle_data_(); bool readall_(uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len); + void log_socket_error_(const char *msg); + void log_read_error_(const char *what); + void log_start_(const char *phase); + void cleanup_connection_(); + void yield_and_feed_watchdog_(); #ifdef USE_OTA_PASSWORD std::string password_; #endif // USE_OTA_PASSWORD uint16_t port_; + uint32_t client_connect_time_{0}; std::unique_ptr server_; std::unique_ptr client_; diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py new file mode 100644 index 0000000000..9d2f17440c --- /dev/null +++ b/esphome/components/espnow/__init__.py @@ -0,0 +1,309 @@ +from esphome import automation, core +import esphome.codegen as cg +from esphome.components import wifi +from esphome.components.udp import CONF_ON_RECEIVE +import esphome.config_validation as cv +from esphome.const import ( + CONF_ADDRESS, + CONF_CHANNEL, + CONF_DATA, + CONF_ENABLE_ON_BOOT, + CONF_ID, + CONF_ON_ERROR, + CONF_TRIGGER_ID, + CONF_WIFI, +) +from esphome.core import CORE, HexInt +from esphome.types import ConfigType + +CODEOWNERS = ["@jesserockz"] + +byte_vector = cg.std_vector.template(cg.uint8) +peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6) + +espnow_ns = cg.esphome_ns.namespace("espnow") +ESPNowComponent = espnow_ns.class_("ESPNowComponent", cg.Component) + +# Handler interfaces that other components can use to register callbacks +ESPNowReceivedPacketHandler = espnow_ns.class_("ESPNowReceivedPacketHandler") +ESPNowUnknownPeerHandler = espnow_ns.class_("ESPNowUnknownPeerHandler") +ESPNowBroadcastedHandler = espnow_ns.class_("ESPNowBroadcastedHandler") + +ESPNowRecvInfo = espnow_ns.class_("ESPNowRecvInfo") +ESPNowRecvInfoConstRef = ESPNowRecvInfo.operator("const").operator("ref") + +SendAction = espnow_ns.class_("SendAction", automation.Action) +SetChannelAction = espnow_ns.class_("SetChannelAction", automation.Action) +AddPeerAction = espnow_ns.class_("AddPeerAction", automation.Action) +DeletePeerAction = espnow_ns.class_("DeletePeerAction", automation.Action) + +ESPNowHandlerTrigger = automation.Trigger.template( + ESPNowRecvInfoConstRef, + cg.uint8.operator("const").operator("ptr"), + cg.uint8, +) + +OnUnknownPeerTrigger = espnow_ns.class_( + "OnUnknownPeerTrigger", ESPNowHandlerTrigger, ESPNowUnknownPeerHandler +) +OnReceiveTrigger = espnow_ns.class_( + "OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivedPacketHandler +) +OnBroadcastedTrigger = espnow_ns.class_( + "OnBroadcastedTrigger", ESPNowHandlerTrigger, ESPNowBroadcastedHandler +) + + +CONF_AUTO_ADD_PEER = "auto_add_peer" +CONF_PEERS = "peers" +CONF_ON_SENT = "on_sent" +CONF_ON_UNKNOWN_PEER = "on_unknown_peer" +CONF_ON_BROADCAST = "on_broadcast" +CONF_CONTINUE_ON_ERROR = "continue_on_error" +CONF_WAIT_FOR_SENT = "wait_for_sent" + +MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESPNowComponent), + cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): wifi.validate_channel, + cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, + cv.Optional(CONF_AUTO_ADD_PEER, default=False): cv.boolean, + cv.Optional(CONF_PEERS): cv.ensure_list(cv.mac_address), + cv.Optional(CONF_ON_UNKNOWN_PEER): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnUnknownPeerTrigger), + }, + single=True, + ), + cv.Optional(CONF_ON_RECEIVE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnReceiveTrigger), + cv.Optional(CONF_ADDRESS): cv.mac_address, + } + ), + cv.Optional(CONF_ON_BROADCAST): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastedTrigger), + cv.Optional(CONF_ADDRESS): cv.mac_address, + } + ), + }, + ).extend(cv.COMPONENT_SCHEMA), + cv.only_on_esp32, +) + + +async def _trigger_to_code(config): + if address := config.get(CONF_ADDRESS): + address = address.parts + trigger = cg.new_Pvariable(config[CONF_TRIGGER_ID], address) + await automation.build_automation( + trigger, + [ + (ESPNowRecvInfoConstRef, "info"), + (cg.uint8.operator("const").operator("ptr"), "data"), + (cg.uint8, "size"), + ], + config, + ) + return trigger + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if CORE.using_arduino: + cg.add_library("WiFi", None) + + cg.add_define("USE_ESPNOW") + if wifi_channel := config.get(CONF_CHANNEL): + cg.add(var.set_wifi_channel(wifi_channel)) + + cg.add(var.set_auto_add_peer(config[CONF_AUTO_ADD_PEER])) + + for peer in config.get(CONF_PEERS, []): + cg.add(var.add_peer(peer.parts)) + + if on_receive := config.get(CONF_ON_UNKNOWN_PEER): + trigger = await _trigger_to_code(on_receive) + cg.add(var.register_unknown_peer_handler(trigger)) + + for on_receive in config.get(CONF_ON_RECEIVE, []): + trigger = await _trigger_to_code(on_receive) + cg.add(var.register_received_handler(trigger)) + + for on_receive in config.get(CONF_ON_BROADCAST, []): + trigger = await _trigger_to_code(on_receive) + cg.add(var.register_broadcasted_handler(trigger)) + + +# ========================================== A C T I O N S ================================================ + + +def validate_peer(value): + if isinstance(value, cv.Lambda): + return cv.returning_lambda(value) + return cv.mac_address(value) + + +def _validate_raw_data(value): + if isinstance(value, str): + if len(value) >= MAX_ESPNOW_PACKET_SIZE: + raise cv.Invalid( + f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} characters long, got {len(value)}" + ) + return value + if isinstance(value, list): + if len(value) > MAX_ESPNOW_PACKET_SIZE: + raise cv.Invalid( + f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} bytes long, got {len(value)}" + ) + return cv.Schema([cv.hex_uint8_t])(value) + raise cv.Invalid( + f"'{CONF_DATA}' must either be a string wrapped in quotes or a list of bytes" + ) + + +async def register_peer(var, config, args): + peer = config[CONF_ADDRESS] + if isinstance(peer, core.MACAddress): + peer = [HexInt(p) for p in peer.parts] + + template_ = await cg.templatable(peer, args, peer_address_t, peer_address_t) + cg.add(var.set_address(template_)) + + +PEER_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(ESPNowComponent), + cv.Required(CONF_ADDRESS): cv.templatable(cv.mac_address), + } +) + +SEND_SCHEMA = PEER_SCHEMA.extend( + { + cv.Required(CONF_DATA): cv.templatable(_validate_raw_data), + cv.Optional(CONF_ON_SENT): automation.validate_action_list, + cv.Optional(CONF_ON_ERROR): automation.validate_action_list, + cv.Optional(CONF_WAIT_FOR_SENT, default=True): cv.boolean, + cv.Optional(CONF_CONTINUE_ON_ERROR, default=True): cv.boolean, + } +) + + +def _validate_send_action(config): + if not config[CONF_WAIT_FOR_SENT] and not config[CONF_CONTINUE_ON_ERROR]: + raise cv.Invalid( + f"'{CONF_CONTINUE_ON_ERROR}' cannot be false if '{CONF_WAIT_FOR_SENT}' is false as the automation will not wait for the failed result.", + path=[CONF_CONTINUE_ON_ERROR], + ) + return config + + +SEND_SCHEMA.add_extra(_validate_send_action) + + +@automation.register_action( + "espnow.send", + SendAction, + SEND_SCHEMA, +) +@automation.register_action( + "espnow.broadcast", + SendAction, + cv.maybe_simple_value( + SEND_SCHEMA.extend( + { + cv.Optional(CONF_ADDRESS, default="FF:FF:FF:FF:FF:FF"): cv.mac_address, + } + ), + key=CONF_DATA, + ), +) +async def send_action( + config: ConfigType, + action_id: core.ID, + template_arg: cg.TemplateArguments, + args: list[tuple], +): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + await register_peer(var, config, args) + + data = config.get(CONF_DATA, []) + if isinstance(data, str): + data = [cg.RawExpression(f"'{c}'") for c in data] + templ = await cg.templatable(data, args, byte_vector, byte_vector) + cg.add(var.set_data(templ)) + + cg.add(var.set_wait_for_sent(config[CONF_WAIT_FOR_SENT])) + cg.add(var.set_continue_on_error(config[CONF_CONTINUE_ON_ERROR])) + + if on_sent_config := config.get(CONF_ON_SENT): + actions = await automation.build_action_list(on_sent_config, template_arg, args) + cg.add(var.add_on_sent(actions)) + if on_error_config := config.get(CONF_ON_ERROR): + actions = await automation.build_action_list( + on_error_config, template_arg, args + ) + cg.add(var.add_on_error(actions)) + return var + + +@automation.register_action( + "espnow.peer.add", + AddPeerAction, + cv.maybe_simple_value( + PEER_SCHEMA, + key=CONF_ADDRESS, + ), +) +@automation.register_action( + "espnow.peer.delete", + DeletePeerAction, + cv.maybe_simple_value( + PEER_SCHEMA, + key=CONF_ADDRESS, + ), +) +async def peer_action( + config: ConfigType, + action_id: core.ID, + template_arg: cg.TemplateArguments, + args: list[tuple], +): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + await register_peer(var, config, args) + + return var + + +@automation.register_action( + "espnow.set_channel", + SetChannelAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(ESPNowComponent), + cv.Required(CONF_CHANNEL): cv.templatable(wifi.validate_channel), + }, + key=CONF_CHANNEL, + ), +) +async def channel_action( + config: ConfigType, + action_id: core.ID, + template_arg: cg.TemplateArguments, + args: list[tuple], +): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) + cg.add(var.set_channel(template_)) + return var diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h new file mode 100644 index 0000000000..2416377859 --- /dev/null +++ b/esphome/components/espnow/automation.h @@ -0,0 +1,175 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "espnow_component.h" + +#include "esphome/core/automation.h" +#include "esphome/core/base_automation.h" + +namespace esphome::espnow { + +template class SendAction : public Action, public Parented { + TEMPLATABLE_VALUE(peer_address_t, address); + TEMPLATABLE_VALUE(std::vector, data); + + public: + void add_on_sent(const std::vector *> &actions) { + this->sent_.add_actions(actions); + if (this->flags_.wait_for_sent) { + this->sent_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); + } + } + void add_on_error(const std::vector *> &actions) { + this->error_.add_actions(actions); + if (this->flags_.wait_for_sent) { + this->error_.add_action(new LambdaAction([this](Ts... x) { + if (this->flags_.continue_on_error) { + this->play_next_(x...); + } else { + this->stop_complex(); + } + })); + } + } + + void set_wait_for_sent(bool wait_for_sent) { this->flags_.wait_for_sent = wait_for_sent; } + void set_continue_on_error(bool continue_on_error) { this->flags_.continue_on_error = continue_on_error; } + + void play_complex(Ts... x) override { + this->num_running_++; + send_callback_t send_callback = [this, x...](esp_err_t status) { + if (status == ESP_OK) { + if (!this->sent_.empty()) { + this->sent_.play(x...); + } else if (this->flags_.wait_for_sent) { + this->play_next_(x...); + } + } else { + if (!this->error_.empty()) { + this->error_.play(x...); + } else if (this->flags_.wait_for_sent) { + if (this->flags_.continue_on_error) { + this->play_next_(x...); + } else { + this->stop_complex(); + } + } + } + }; + peer_address_t address = this->address_.value(x...); + std::vector data = this->data_.value(x...); + esp_err_t err = this->parent_->send(address.data(), data, send_callback); + if (err != ESP_OK) { + send_callback(err); + } else if (!this->flags_.wait_for_sent) { + this->play_next_(x...); + } + } + + void play(Ts... x) override { /* ignore - see play_complex */ + } + + void stop() override { + this->sent_.stop(); + this->error_.stop(); + } + + protected: + ActionList sent_; + ActionList error_; + + struct { + uint8_t wait_for_sent : 1; // Wait for the send operation to complete before continuing automation + uint8_t continue_on_error : 1; // Continue automation even if the send operation fails + uint8_t reserved : 6; // Reserved for future use + } flags_{0}; +}; + +template class AddPeerAction : public Action, public Parented { + TEMPLATABLE_VALUE(peer_address_t, address); + + public: + void play(Ts... x) override { + peer_address_t address = this->address_.value(x...); + this->parent_->add_peer(address.data()); + } +}; + +template class DeletePeerAction : public Action, public Parented { + TEMPLATABLE_VALUE(peer_address_t, address); + + public: + void play(Ts... x) override { + peer_address_t address = this->address_.value(x...); + this->parent_->del_peer(address.data()); + } +}; + +template class SetChannelAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + void play(Ts... x) override { + if (this->parent_->is_wifi_enabled()) { + return; + } + this->parent_->set_wifi_channel(this->channel_.value(x...)); + this->parent_->apply_wifi_channel(); + } +}; + +class OnReceiveTrigger : public Trigger, + public ESPNowReceivedPacketHandler { + public: + explicit OnReceiveTrigger(std::array address) : has_address_(true) { + memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN); + } + + explicit OnReceiveTrigger() : has_address_(false) {} + + bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override { + bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0); + if (!match) + return false; + + this->trigger(info, data, size); + return false; // Return false to continue processing other internal handlers + } + + protected: + bool has_address_{false}; + const uint8_t *address_[ESP_NOW_ETH_ALEN]; +}; +class OnUnknownPeerTrigger : public Trigger, + public ESPNowUnknownPeerHandler { + public: + bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override { + this->trigger(info, data, size); + return false; // Return false to continue processing other internal handlers + } +}; +class OnBroadcastedTrigger : public Trigger, + public ESPNowBroadcastedHandler { + public: + explicit OnBroadcastedTrigger(std::array address) : has_address_(true) { + memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN); + } + explicit OnBroadcastedTrigger() : has_address_(false) {} + + bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override { + bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0); + if (!match) + return false; + + this->trigger(info, data, size); + return false; // Return false to continue processing other internal handlers + } + + protected: + bool has_address_{false}; + const uint8_t *address_[ESP_NOW_ETH_ALEN]; +}; + +} // namespace esphome::espnow + +#endif // USE_ESP32 diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp new file mode 100644 index 0000000000..82f8e3230e --- /dev/null +++ b/esphome/components/espnow/espnow_component.cpp @@ -0,0 +1,468 @@ +#include "espnow_component.h" + +#ifdef USE_ESP32 + +#include "espnow_err.h" + +#include "esphome/core/defines.h" +#include "esphome/core/log.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif + +namespace esphome::espnow { + +static constexpr const char *TAG = "espnow"; + +static const esp_err_t CONFIG_ESPNOW_WAKE_WINDOW = 50; +static const esp_err_t CONFIG_ESPNOW_WAKE_INTERVAL = 100; + +ESPNowComponent *global_esp_now = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +static const LogString *espnow_error_to_str(esp_err_t error) { + switch (error) { + case ESP_ERR_ESPNOW_FAILED: + return LOG_STR("ESPNow is in fail mode"); + case ESP_ERR_ESPNOW_OWN_ADDRESS: + return LOG_STR("Message to your self"); + case ESP_ERR_ESPNOW_DATA_SIZE: + return LOG_STR("Data size to large"); + case ESP_ERR_ESPNOW_PEER_NOT_SET: + return LOG_STR("Peer address not set"); + case ESP_ERR_ESPNOW_PEER_NOT_PAIRED: + return LOG_STR("Peer address not paired"); + case ESP_ERR_ESPNOW_NOT_INIT: + return LOG_STR("Not init"); + case ESP_ERR_ESPNOW_ARG: + return LOG_STR("Invalid argument"); + case ESP_ERR_ESPNOW_INTERNAL: + return LOG_STR("Internal Error"); + case ESP_ERR_ESPNOW_NO_MEM: + return LOG_STR("Our of memory"); + case ESP_ERR_ESPNOW_NOT_FOUND: + return LOG_STR("Peer not found"); + case ESP_ERR_ESPNOW_IF: + return LOG_STR("Interface does not match"); + case ESP_OK: + return LOG_STR("OK"); + case ESP_NOW_SEND_FAIL: + return LOG_STR("Failed"); + default: + return LOG_STR("Unknown Error"); + } +} + +std::string peer_str(uint8_t *peer) { + if (peer == nullptr || peer[0] == 0) { + return "[Not Set]"; + } else if (memcmp(peer, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { + return "[Broadcast]"; + } else if (memcmp(peer, ESPNOW_MULTICAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { + return "[Multicast]"; + } else { + return format_mac_address_pretty(peer); + } +} + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) +void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status) +#else +void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) +#endif +{ + // Allocate an event from the pool + ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate(); + if (packet == nullptr) { + // No events available - queue is full or we're out of memory + global_esp_now->receive_packet_queue_.increment_dropped_count(); + return; + } + +// Load new packet data (replaces previous packet) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + packet->load_sent_data(info->des_addr, status); +#else + packet->load_sent_data(mac_addr, status); +#endif + + // Push the packet to the queue + global_esp_now->receive_packet_queue_.push(packet); + // Push always because we're the only producer and the pool ensures we never exceed queue size +} + +void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { + // Allocate an event from the pool + ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate(); + if (packet == nullptr) { + // No events available - queue is full or we're out of memory + global_esp_now->receive_packet_queue_.increment_dropped_count(); + return; + } + + // Load new packet data (replaces previous packet) + packet->load_received_data(info, data, size); + + // Push the packet to the queue + global_esp_now->receive_packet_queue_.push(packet); + // Push always because we're the only producer and the pool ensures we never exceed queue size +} + +ESPNowComponent::ESPNowComponent() { global_esp_now = this; } + +void ESPNowComponent::dump_config() { + uint32_t version = 0; + esp_now_get_version(&version); + + ESP_LOGCONFIG(TAG, "espnow:"); + if (this->is_disabled()) { + ESP_LOGCONFIG(TAG, " Disabled"); + return; + } + ESP_LOGCONFIG(TAG, + " Own address: %s\n" + " Version: v%" PRIu32 "\n" + " Wi-Fi channel: %d", + format_mac_address_pretty(this->own_address_).c_str(), version, this->wifi_channel_); +#ifdef USE_WIFI + ESP_LOGCONFIG(TAG, " Wi-Fi enabled: %s", YESNO(this->is_wifi_enabled())); +#endif +} + +bool ESPNowComponent::is_wifi_enabled() { +#ifdef USE_WIFI + return wifi::global_wifi_component != nullptr && !wifi::global_wifi_component->is_disabled(); +#else + return false; +#endif +} + +void ESPNowComponent::setup() { + if (this->enable_on_boot_) { + this->enable_(); + } else { + this->state_ = ESPNOW_STATE_DISABLED; + } +} + +void ESPNowComponent::enable() { + if (this->state_ == ESPNOW_STATE_ENABLED) + return; + + ESP_LOGD(TAG, "Enabling"); + this->state_ = ESPNOW_STATE_OFF; + + this->enable_(); +} + +void ESPNowComponent::enable_() { + if (!this->is_wifi_enabled()) { + esp_event_loop_create_default(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); + ESP_ERROR_CHECK(esp_wifi_start()); + ESP_ERROR_CHECK(esp_wifi_disconnect()); + + this->apply_wifi_channel(); + } + this->get_wifi_channel(); + + esp_err_t err = esp_now_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_now_init failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + + err = esp_now_register_recv_cb(on_data_received); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_now_register_recv_cb failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + + err = esp_now_register_send_cb(on_send_report); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_now_register_recv_cb failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + + esp_wifi_get_mac(WIFI_IF_STA, this->own_address_); + +#ifdef USE_DEEP_SLEEP + esp_now_set_wake_window(CONFIG_ESPNOW_WAKE_WINDOW); + esp_wifi_connectionless_module_set_wake_interval(CONFIG_ESPNOW_WAKE_INTERVAL); +#endif + + for (auto peer : this->peers_) { + this->add_peer(peer.address); + } + + this->state_ = ESPNOW_STATE_ENABLED; +} + +void ESPNowComponent::disable() { + if (this->state_ == ESPNOW_STATE_DISABLED) + return; + + ESP_LOGD(TAG, "Disabling"); + this->state_ = ESPNOW_STATE_DISABLED; + + esp_now_unregister_recv_cb(); + esp_now_unregister_send_cb(); + + esp_err_t err = esp_now_deinit(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_now_deinit failed! 0x%x", err); + } +} + +void ESPNowComponent::apply_wifi_channel() { + if (this->state_ == ESPNOW_STATE_DISABLED) { + ESP_LOGE(TAG, "Cannot set channel when ESPNOW disabled"); + this->mark_failed(); + return; + } + + if (this->is_wifi_enabled()) { + ESP_LOGE(TAG, "Cannot set channel when Wi-Fi enabled"); + this->mark_failed(); + return; + } + + ESP_LOGI(TAG, "Channel set to %d.", this->wifi_channel_); + esp_wifi_set_promiscuous(true); + esp_wifi_set_channel(this->wifi_channel_, WIFI_SECOND_CHAN_NONE); + esp_wifi_set_promiscuous(false); +} + +void ESPNowComponent::loop() { +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->is_connected()) { + int32_t new_channel = wifi::global_wifi_component->get_wifi_channel(); + if (new_channel != this->wifi_channel_) { + ESP_LOGI(TAG, "Wifi Channel is changed from %d to %d.", this->wifi_channel_, new_channel); + this->wifi_channel_ = new_channel; + } + } +#endif + // Process received packets + ESPNowPacket *packet = this->receive_packet_queue_.pop(); + while (packet != nullptr) { + switch (packet->type_) { + case ESPNowPacket::RECEIVED: { + const ESPNowRecvInfo info = packet->get_receive_info(); + if (!esp_now_is_peer_exist(info.src_addr)) { + bool handled = false; + for (auto *handler : this->unknown_peer_handlers_) { + if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size)) { + handled = true; + break; // If a handler returns true, stop processing further handlers + } + } + if (!handled && this->auto_add_peer_) { + this->add_peer(info.src_addr); + } + } + // Intentionally left as if instead of else in case the peer is added above + if (esp_now_is_peer_exist(info.src_addr)) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "<<< [%s -> %s] %s", format_mac_address_pretty(info.src_addr).c_str(), + format_mac_address_pretty(info.des_addr).c_str(), + format_hex_pretty(packet->packet_.receive.data, packet->packet_.receive.size).c_str()); +#endif + if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { + for (auto *handler : this->broadcasted_handlers_) { + if (handler->on_broadcasted(info, packet->packet_.receive.data, packet->packet_.receive.size)) + break; // If a handler returns true, stop processing further handlers + } + } else { + for (auto *handler : this->received_handlers_) { + if (handler->on_received(info, packet->packet_.receive.data, packet->packet_.receive.size)) + break; // If a handler returns true, stop processing further handlers + } + } + } + break; + } + case ESPNowPacket::SENT: { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, ">>> [%s] %s", format_mac_address_pretty(packet->packet_.sent.address).c_str(), + LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status))); +#endif + if (this->current_send_packet_ != nullptr) { + this->current_send_packet_->callback_(packet->packet_.sent.status); + this->send_packet_pool_.release(this->current_send_packet_); + this->current_send_packet_ = nullptr; // Reset current packet after sending + } + break; + } + default: + break; + } + // Return the packet to the pool + this->receive_packet_pool_.release(packet); + packet = this->receive_packet_queue_.pop(); + } + + // Process sending packet queue + if (this->current_send_packet_ == nullptr) { + this->send_(); + } + + // Log dropped received packets periodically + uint16_t received_dropped = this->receive_packet_queue_.get_and_reset_dropped_count(); + if (received_dropped > 0) { + ESP_LOGW(TAG, "Dropped %u received packets due to buffer overflow", received_dropped); + } + + // Log dropped send packets periodically + uint16_t send_dropped = this->send_packet_queue_.get_and_reset_dropped_count(); + if (send_dropped > 0) { + ESP_LOGW(TAG, "Dropped %u send packets due to buffer overflow", send_dropped); + } +} + +uint8_t ESPNowComponent::get_wifi_channel() { + wifi_second_chan_t dummy; + esp_wifi_get_channel(&this->wifi_channel_, &dummy); + return this->wifi_channel_; +} + +esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payload, size_t size, + const send_callback_t &callback) { + if (this->state_ != ESPNOW_STATE_ENABLED) { + return ESP_ERR_ESPNOW_NOT_INIT; + } else if (this->is_failed()) { + return ESP_ERR_ESPNOW_FAILED; + } else if (peer_address == 0ULL) { + return ESP_ERR_ESPNOW_PEER_NOT_SET; + } else if (memcmp(peer_address, this->own_address_, ESP_NOW_ETH_ALEN) == 0) { + return ESP_ERR_ESPNOW_OWN_ADDRESS; + } else if (size > ESP_NOW_MAX_DATA_LEN) { + return ESP_ERR_ESPNOW_DATA_SIZE; + } else if (!esp_now_is_peer_exist(peer_address)) { + if (memcmp(peer_address, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0 || this->auto_add_peer_) { + esp_err_t err = this->add_peer(peer_address); + if (err != ESP_OK) { + return err; + } + } else { + return ESP_ERR_ESPNOW_PEER_NOT_PAIRED; + } + } + // Allocate a packet from the pool + ESPNowSendPacket *packet = this->send_packet_pool_.allocate(); + if (packet == nullptr) { + this->send_packet_queue_.increment_dropped_count(); + ESP_LOGE(TAG, "Failed to allocate send packet from pool"); + this->status_momentary_warning("send-packet-pool-full"); + return ESP_ERR_ESPNOW_NO_MEM; + } + // Load the packet data + packet->load_data(peer_address, payload, size, callback); + // Push the packet to the send queue + this->send_packet_queue_.push(packet); + return ESP_OK; +} + +void ESPNowComponent::send_() { + ESPNowSendPacket *packet = this->send_packet_queue_.pop(); + if (packet == nullptr) { + return; // No packets to send + } + + this->current_send_packet_ = packet; + esp_err_t err = esp_now_send(packet->address_, packet->data_, packet->size_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to send packet to %s - %s", format_mac_address_pretty(packet->address_).c_str(), + LOG_STR_ARG(espnow_error_to_str(err))); + if (packet->callback_ != nullptr) { + packet->callback_(err); + } + this->status_momentary_warning("send-failed"); + this->send_packet_pool_.release(packet); + this->current_send_packet_ = nullptr; // Reset current packet + return; + } +} + +esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) { + if (this->state_ != ESPNOW_STATE_ENABLED || this->is_failed()) { + return ESP_ERR_ESPNOW_NOT_INIT; + } + + if (memcmp(peer, this->own_address_, ESP_NOW_ETH_ALEN) == 0) { + this->mark_failed(); + return ESP_ERR_INVALID_MAC; + } + + if (!esp_now_is_peer_exist(peer)) { + esp_now_peer_info_t peer_info = {}; + memset(&peer_info, 0, sizeof(esp_now_peer_info_t)); + peer_info.ifidx = WIFI_IF_STA; + memcpy(peer_info.peer_addr, peer, ESP_NOW_ETH_ALEN); + esp_err_t err = esp_now_add_peer(&peer_info); + + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to add peer %s - %s", format_mac_address_pretty(peer).c_str(), + LOG_STR_ARG(espnow_error_to_str(err))); + this->status_momentary_warning("peer-add-failed"); + return err; + } + } + bool found = false; + for (auto &it : this->peers_) { + if (it == peer) { + found = true; + break; + } + } + if (!found) { + ESPNowPeer new_peer; + memcpy(new_peer.address, peer, ESP_NOW_ETH_ALEN); + this->peers_.push_back(new_peer); + } + + return ESP_OK; +} + +esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) { + if (this->state_ != ESPNOW_STATE_ENABLED || this->is_failed()) { + return ESP_ERR_ESPNOW_NOT_INIT; + } + if (esp_now_is_peer_exist(peer)) { + esp_err_t err = esp_now_del_peer(peer); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to delete peer %s - %s", format_mac_address_pretty(peer).c_str(), + LOG_STR_ARG(espnow_error_to_str(err))); + this->status_momentary_warning("peer-del-failed"); + return err; + } + } + for (auto it = this->peers_.begin(); it != this->peers_.end(); ++it) { + if (*it == peer) { + this->peers_.erase(it); + break; + } + } + return ESP_OK; +} + +} // namespace esphome::espnow + +#endif // USE_ESP32 diff --git a/esphome/components/espnow/espnow_component.h b/esphome/components/espnow/espnow_component.h new file mode 100644 index 0000000000..9941e97227 --- /dev/null +++ b/esphome/components/espnow/espnow_component.h @@ -0,0 +1,183 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +#ifdef USE_ESP32 + +#include "esphome/core/event_pool.h" +#include "esphome/core/lock_free_queue.h" +#include "espnow_packet.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include + +namespace esphome::espnow { + +// Maximum size of the ESPNow event queue - must be power of 2 for lock-free queue +static constexpr size_t MAX_ESP_NOW_SEND_QUEUE_SIZE = 16; +static constexpr size_t MAX_ESP_NOW_RECEIVE_QUEUE_SIZE = 16; + +using peer_address_t = std::array; + +enum class ESPNowTriggers : uint8_t { + TRIGGER_NONE = 0, + ON_NEW_PEER = 1, + ON_RECEIVED = 2, + ON_BROADCASTED = 3, + ON_SUCCEED = 10, + ON_FAILED = 11, +}; + +enum ESPNowState : uint8_t { + /** Nothing has been initialized yet. */ + ESPNOW_STATE_OFF = 0, + /** ESPNOW is disabled. */ + ESPNOW_STATE_DISABLED, + /** ESPNOW is enabled. */ + ESPNOW_STATE_ENABLED, +}; + +struct ESPNowPeer { + uint8_t address[ESP_NOW_ETH_ALEN]; // MAC address of the peer + + bool operator==(const ESPNowPeer &other) const { return memcmp(this->address, other.address, ESP_NOW_ETH_ALEN) == 0; } + bool operator==(const uint8_t *other) const { return memcmp(this->address, other, ESP_NOW_ETH_ALEN) == 0; } +}; + +/// Handler interface for receiving ESPNow packets from unknown peers +/// Components should inherit from this class to handle incoming ESPNow data +class ESPNowUnknownPeerHandler { + public: + /// Called when an ESPNow packet is received from an unknown peer + /// @param info Information about the received packet (sender MAC, etc.) + /// @param data Pointer to the received data payload + /// @param size Size of the received data in bytes + /// @return true if the packet was handled, false otherwise + virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; +}; + +/// Handler interface for receiving ESPNow packets +/// Components should inherit from this class to handle incoming ESPNow data +class ESPNowReceivedPacketHandler { + public: + /// Called when an ESPNow packet is received + /// @param info Information about the received packet (sender MAC, etc.) + /// @param data Pointer to the received data payload + /// @param size Size of the received data in bytes + /// @return true if the packet was handled, false otherwise + virtual bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; +}; +/// Handler interface for receiving broadcasted ESPNow packets +/// Components should inherit from this class to handle incoming ESPNow data +class ESPNowBroadcastedHandler { + public: + /// Called when a broadcasted ESPNow packet is received + /// @param info Information about the received packet (sender MAC, etc.) + /// @param data Pointer to the received data payload + /// @param size Size of the received data in bytes + /// @return true if the packet was handled, false otherwise + virtual bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0; +}; + +class ESPNowComponent : public Component { + public: + ESPNowComponent(); + void setup() override; + void loop() override; + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::LATE; } + + // Add a peer to the internal list of peers + void add_peer(peer_address_t address) { + ESPNowPeer peer; + memcpy(peer.address, address.data(), ESP_NOW_ETH_ALEN); + this->peers_.push_back(peer); + } + // Add a peer with the esp_now api and add to the internal list if doesnt exist already + esp_err_t add_peer(const uint8_t *peer); + // Remove a peer with the esp_now api and remove from the internal list if exists + esp_err_t del_peer(const uint8_t *peer); + + void set_wifi_channel(uint8_t channel) { this->wifi_channel_ = channel; } + void apply_wifi_channel(); + uint8_t get_wifi_channel(); + + void set_auto_add_peer(bool value) { this->auto_add_peer_ = value; } + + void enable(); + void disable(); + bool is_disabled() const { return this->state_ == ESPNOW_STATE_DISABLED; }; + void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } + bool is_wifi_enabled(); + + /// @brief Queue a packet to be sent to a specific peer address. + /// This method will add the packet to the internal queue and + /// call the callback when the packet is sent. + /// Only one packet will be sent at any given time and the next one will not be sent until + /// the previous one has been acknowledged or failed. + /// @param peer_address MAC address of the peer to send the packet to + /// @param payload Data payload to send + /// @param callback Callback to call when the send operation is complete + /// @return ESP_OK on success, or an error code on failure + esp_err_t send(const uint8_t *peer_address, const std::vector &payload, + const send_callback_t &callback = nullptr) { + return this->send(peer_address, payload.data(), payload.size(), callback); + } + esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size, + const send_callback_t &callback = nullptr); + + void register_received_handler(ESPNowReceivedPacketHandler *handler) { this->received_handlers_.push_back(handler); } + void register_unknown_peer_handler(ESPNowUnknownPeerHandler *handler) { + this->unknown_peer_handlers_.push_back(handler); + } + void register_broadcasted_handler(ESPNowBroadcastedHandler *handler) { + this->broadcasted_handlers_.push_back(handler); + } + + protected: + friend void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + friend void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status); +#else + friend void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status); +#endif + + void enable_(); + void send_(); + + std::vector unknown_peer_handlers_; + std::vector received_handlers_; + std::vector broadcasted_handlers_; + + std::vector peers_{}; + + uint8_t own_address_[ESP_NOW_ETH_ALEN]{0}; + LockFreeQueue receive_packet_queue_{}; + EventPool receive_packet_pool_{}; + + LockFreeQueue send_packet_queue_{}; + EventPool send_packet_pool_{}; + ESPNowSendPacket *current_send_packet_{nullptr}; // Currently sending packet, nullptr if none + + uint8_t wifi_channel_{0}; + ESPNowState state_{ESPNOW_STATE_OFF}; + + bool auto_add_peer_{false}; + bool enable_on_boot_{true}; +}; + +extern ESPNowComponent *global_esp_now; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::espnow + +#endif // USE_ESP32 diff --git a/esphome/components/espnow/espnow_err.h b/esphome/components/espnow/espnow_err.h new file mode 100644 index 0000000000..ceda1b7683 --- /dev/null +++ b/esphome/components/espnow/espnow_err.h @@ -0,0 +1,19 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include + +namespace esphome::espnow { + +static const esp_err_t ESP_ERR_ESPNOW_CMP_BASE = (ESP_ERR_ESPNOW_BASE + 20); +static const esp_err_t ESP_ERR_ESPNOW_FAILED = (ESP_ERR_ESPNOW_CMP_BASE + 1); +static const esp_err_t ESP_ERR_ESPNOW_OWN_ADDRESS = (ESP_ERR_ESPNOW_CMP_BASE + 2); +static const esp_err_t ESP_ERR_ESPNOW_DATA_SIZE = (ESP_ERR_ESPNOW_CMP_BASE + 3); +static const esp_err_t ESP_ERR_ESPNOW_PEER_NOT_SET = (ESP_ERR_ESPNOW_CMP_BASE + 4); +static const esp_err_t ESP_ERR_ESPNOW_PEER_NOT_PAIRED = (ESP_ERR_ESPNOW_CMP_BASE + 5); + +} // namespace esphome::espnow + +#endif // USE_ESP32 diff --git a/esphome/components/espnow/espnow_packet.h b/esphome/components/espnow/espnow_packet.h new file mode 100644 index 0000000000..b6192a0d41 --- /dev/null +++ b/esphome/components/espnow/espnow_packet.h @@ -0,0 +1,166 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "espnow_err.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace esphome::espnow { + +static const uint8_t ESPNOW_BROADCAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const uint8_t ESPNOW_MULTICAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE}; + +struct WifiPacketRxControl { + int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm + uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or + // light sleep is not enabled +}; + +struct ESPNowRecvInfo { + uint8_t src_addr[ESP_NOW_ETH_ALEN]; /**< Source address of ESPNOW packet */ + uint8_t des_addr[ESP_NOW_ETH_ALEN]; /**< Destination address of ESPNOW packet */ + wifi_pkt_rx_ctrl_t *rx_ctrl; /**< Rx control info of ESPNOW packet */ +}; + +using send_callback_t = std::function; + +class ESPNowPacket { + public: + // NOLINTNEXTLINE(readability-identifier-naming) + enum esp_now_packet_type_t : uint8_t { + RECEIVED, + SENT, + }; + + // Constructor for received data + ESPNowPacket(const esp_now_recv_info_t *info, const uint8_t *data, int size) { + this->init_received_data_(info, data, size); + }; + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + // Constructor for sent data + ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) { + this->init_sent_data_(info->src_addr, status); + } +#else + // Constructor for sent data + ESPNowPacket(const uint8_t *mac_addr, esp_now_send_status_t status) { this->init_sent_data_(mac_addr, status); } +#endif + + // Default constructor for pre-allocation in pool + ESPNowPacket() {} + + void release() {} + + void load_received_data(const esp_now_recv_info_t *info, const uint8_t *data, int size) { + this->type_ = RECEIVED; + this->init_received_data_(info, data, size); + } + + void load_sent_data(const uint8_t *mac_addr, esp_now_send_status_t status) { + this->type_ = SENT; + this->init_sent_data_(mac_addr, status); + } + + // Disable copy to prevent double-delete + ESPNowPacket(const ESPNowPacket &) = delete; + ESPNowPacket &operator=(const ESPNowPacket &) = delete; + + union { + // NOLINTNEXTLINE(readability-identifier-naming) + struct received_data { + ESPNowRecvInfo info; // Information about the received packet + uint8_t data[ESP_NOW_MAX_DATA_LEN]; // Data received in the packet + uint8_t size; // Size of the received data + WifiPacketRxControl rx_ctrl; // Status of the received packet + } receive; + + // NOLINTNEXTLINE(readability-identifier-naming) + struct sent_data { + uint8_t address[ESP_NOW_ETH_ALEN]; + esp_now_send_status_t status; + } sent; + } packet_; + + esp_now_packet_type_t type_; + + esp_now_packet_type_t type() const { return this->type_; } + const ESPNowRecvInfo &get_receive_info() const { return this->packet_.receive.info; } + + private: + void init_received_data_(const esp_now_recv_info_t *info, const uint8_t *data, int size) { + memcpy(this->packet_.receive.info.src_addr, info->src_addr, ESP_NOW_ETH_ALEN); + memcpy(this->packet_.receive.info.des_addr, info->des_addr, ESP_NOW_ETH_ALEN); + memcpy(this->packet_.receive.data, data, size); + this->packet_.receive.size = size; + + this->packet_.receive.rx_ctrl.rssi = info->rx_ctrl->rssi; + this->packet_.receive.rx_ctrl.timestamp = info->rx_ctrl->timestamp; + + this->packet_.receive.info.rx_ctrl = reinterpret_cast(&this->packet_.receive.rx_ctrl); + } + + void init_sent_data_(const uint8_t *mac_addr, esp_now_send_status_t status) { + memcpy(this->packet_.sent.address, mac_addr, ESP_NOW_ETH_ALEN); + this->packet_.sent.status = status; + } +}; + +class ESPNowSendPacket { + public: + ESPNowSendPacket(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &&callback) + : callback_(callback) { + this->init_data_(peer_address, payload, size); + } + ESPNowSendPacket(const uint8_t *peer_address, const uint8_t *payload, size_t size) { + this->init_data_(peer_address, payload, size); + } + + // Default constructor for pre-allocation in pool + ESPNowSendPacket() {} + + void release() {} + + // Disable copy to prevent double-delete + ESPNowSendPacket(const ESPNowSendPacket &) = delete; + ESPNowSendPacket &operator=(const ESPNowSendPacket &) = delete; + + void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &callback) { + this->init_data_(peer_address, payload, size); + this->callback_ = callback; + } + + void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size) { + this->init_data_(peer_address, payload, size); + this->callback_ = nullptr; // Reset callback + } + + uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to + uint8_t data_[ESP_NOW_MAX_DATA_LEN]{0}; // Data to send + uint8_t size_{0}; // Size of the data to send, must be <= ESP_NOW_MAX_DATA_LEN + send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete + + private: + void init_data_(const uint8_t *peer_address, const uint8_t *payload, size_t size) { + memcpy(this->address_, peer_address, ESP_NOW_ETH_ALEN); + if (size > ESP_NOW_MAX_DATA_LEN) { + this->size_ = 0; + return; + } + this->size_ = size; + memcpy(this->data_, payload, this->size_); + } +}; + +} // namespace esphome::espnow + +#endif // USE_ESP32 diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index ac07d02e37..7a412a643d 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -112,7 +112,7 @@ def _is_framework_spi_polling_mode_supported(): return True if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1): return True - if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): + if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): # noqa: SIM103 return True return False if CORE.using_arduino: @@ -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/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index 4f31e0a9fd..5e73e99101 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -25,6 +25,9 @@ #include "driver/gpio.h" #include "esp_rom_gpio.h" #include "esp_rom_sys.h" +#include "esp_idf_version.h" + +#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) static const char *TAG = "jl1101"; #define PHY_CHECK(a, str, goto_tag, ...) \ @@ -336,4 +339,6 @@ esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) { err: return NULL; } + +#endif /* USE_ARDUINO */ #endif /* USE_ESP32 */ diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index ff37dcfdd1..87913488da 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -54,7 +54,6 @@ EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non- EthernetComponent::EthernetComponent() { global_eth_component = this; } void EthernetComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (esp_reset_reason() != ESP_RST_DEEPSLEEP) { // Delay here to allow power to stabilise before Ethernet is initialized. delay(300); // NOLINT diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 1b347946f5..bdcda6afb4 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -11,6 +11,7 @@ #include "esp_eth_mac.h" #include "esp_netif.h" #include "esp_mac.h" +#include "esp_idf_version.h" namespace esphome { namespace ethernet { @@ -153,7 +154,10 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; + +#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); +#endif } // namespace ethernet } // namespace esphome 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/event/__init__.py b/esphome/components/event/__init__.py index 3aff96a48e..1948570ecd 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -145,5 +145,4 @@ async def event_fire_to_code(config, action_id, template_arg, args): @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_EVENT") cg.add_global(event_ns.using) diff --git a/esphome/components/factory_reset/__init__.py b/esphome/components/factory_reset/__init__.py index f1bcfd8c55..f3cefe6970 100644 --- a/esphome/components/factory_reset/__init__.py +++ b/esphome/components/factory_reset/__init__.py @@ -1,5 +1,97 @@ +from esphome.automation import Trigger, build_automation, validate_automation import esphome.codegen as cg +from esphome.components.esp8266 import CONF_RESTORE_FROM_FLASH, KEY_ESP8266 +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_TRIGGER_ID, + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_LN882X, + PLATFORM_RTL87XX, +) +from esphome.core import CORE +from esphome.final_validate import full_config CODEOWNERS = ["@anatoly-savchenkov"] factory_reset_ns = cg.esphome_ns.namespace("factory_reset") +FactoryResetComponent = factory_reset_ns.class_("FactoryResetComponent", cg.Component) +FastBootTrigger = factory_reset_ns.class_("FastBootTrigger", Trigger, cg.Component) + +CONF_MAX_DELAY = "max_delay" +CONF_RESETS_REQUIRED = "resets_required" +CONF_ON_INCREMENT = "on_increment" + + +def _validate(config): + if CONF_RESETS_REQUIRED in config: + return cv.only_on( + [ + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_LN882X, + PLATFORM_RTL87XX, + ] + )(config) + + if CONF_ON_INCREMENT in config: + raise cv.Invalid( + f"'{CONF_ON_INCREMENT}' requires a value for '{CONF_RESETS_REQUIRED}'" + ) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(FactoryResetComponent), + cv.Optional(CONF_MAX_DELAY, default="10s"): cv.All( + cv.positive_time_period_seconds, + cv.Range(min=cv.TimePeriod(milliseconds=1000)), + ), + cv.Optional(CONF_RESETS_REQUIRED): cv.positive_not_null_int, + cv.Optional(CONF_ON_INCREMENT): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FastBootTrigger), + } + ), + } + ).extend(cv.COMPONENT_SCHEMA), + _validate, +) + + +def _final_validate(config): + if CORE.is_esp8266 and CONF_RESETS_REQUIRED in config: + fconfig = full_config.get() + if not fconfig.get_config_for_path([KEY_ESP8266, CONF_RESTORE_FROM_FLASH]): + raise cv.Invalid( + "'resets_required' needs 'restore_from_flash' to be enabled in the 'esp8266' configuration" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + if reset_count := config.get(CONF_RESETS_REQUIRED): + var = cg.new_Pvariable( + config[CONF_ID], + reset_count, + config[CONF_MAX_DELAY].total_milliseconds, + ) + await cg.register_component(var, config) + for conf in config.get(CONF_ON_INCREMENT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await build_automation( + trigger, + [ + (cg.uint8, "x"), + (cg.uint8, "target"), + ], + conf, + ) diff --git a/esphome/components/factory_reset/factory_reset.cpp b/esphome/components/factory_reset/factory_reset.cpp new file mode 100644 index 0000000000..c900759d90 --- /dev/null +++ b/esphome/components/factory_reset/factory_reset.cpp @@ -0,0 +1,76 @@ +#include "factory_reset.h" + +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#include + +#if !defined(USE_RP2040) && !defined(USE_HOST) + +namespace esphome { +namespace factory_reset { + +static const char *const TAG = "factory_reset"; +static const uint32_t POWER_CYCLES_KEY = 0xFA5C0DE; + +static bool was_power_cycled() { +#ifdef USE_ESP32 + return esp_reset_reason() == ESP_RST_POWERON; +#endif +#ifdef USE_ESP8266 + auto reset_reason = EspClass::getResetReason(); + return strcasecmp(reset_reason.c_str(), "power On") == 0 || strcasecmp(reset_reason.c_str(), "external system") == 0; +#endif +#ifdef USE_LIBRETINY + auto reason = lt_get_reboot_reason(); + return reason == REBOOT_REASON_POWER || reason == REBOOT_REASON_HARDWARE; +#endif +} + +void FactoryResetComponent::dump_config() { + uint8_t count = 0; + this->flash_.load(&count); + ESP_LOGCONFIG(TAG, "Factory Reset by Reset:"); + ESP_LOGCONFIG(TAG, + " Max interval between resets %" PRIu32 " seconds\n" + " Current count: %u\n" + " Factory reset after %u resets", + this->max_interval_ / 1000, count, this->required_count_); +} + +void FactoryResetComponent::save_(uint8_t count) { + this->flash_.save(&count); + global_preferences->sync(); + this->defer([count, this] { this->increment_callback_.call(count, this->required_count_); }); +} + +void FactoryResetComponent::setup() { + this->flash_ = global_preferences->make_preference(POWER_CYCLES_KEY, true); + if (was_power_cycled()) { + uint8_t count = 0; + this->flash_.load(&count); + // this is a power on reset or external system reset + count++; + if (count == this->required_count_) { + ESP_LOGW(TAG, "Reset count reached, factory resetting"); + global_preferences->reset(); + // delay to allow log to be sent + delay(100); // NOLINT + App.safe_reboot(); // should not return + } + this->save_(count); + ESP_LOGD(TAG, "Power on reset detected, incremented count to %u", count); + this->set_timeout(this->max_interval_, [this]() { + ESP_LOGD(TAG, "No reset in the last %" PRIu32 " seconds, resetting count", this->max_interval_ / 1000); + this->save_(0); // reset count + }); + } else { + this->save_(0); // reset count if not a power cycle + } +} + +} // namespace factory_reset +} // namespace esphome + +#endif // !defined(USE_RP2040) && !defined(USE_HOST) diff --git a/esphome/components/factory_reset/factory_reset.h b/esphome/components/factory_reset/factory_reset.h new file mode 100644 index 0000000000..80942b29bd --- /dev/null +++ b/esphome/components/factory_reset/factory_reset.h @@ -0,0 +1,43 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/core/preferences.h" +#if !defined(USE_RP2040) && !defined(USE_HOST) + +#ifdef USE_ESP32 +#include +#endif + +namespace esphome { +namespace factory_reset { +class FactoryResetComponent : public Component { + public: + FactoryResetComponent(uint8_t required_count, uint32_t max_interval) + : required_count_(required_count), max_interval_(max_interval) {} + + void dump_config() override; + void setup() override; + void add_increment_callback(std::function &&callback) { + this->increment_callback_.add(std::move(callback)); + } + + protected: + ~FactoryResetComponent() = default; + void save_(uint8_t count); + ESPPreferenceObject flash_{}; // saves the number of fast power cycles + uint8_t required_count_; // The number of boot attempts before fast boot is enabled + uint32_t max_interval_; // max interval between power cycles + CallbackManager increment_callback_{}; +}; + +class FastBootTrigger : public Trigger { + public: + explicit FastBootTrigger(FactoryResetComponent *parent) { + parent->add_increment_callback([this](uint8_t current, uint8_t target) { this->trigger(current, target); }); + } +}; +} // namespace factory_reset +} // namespace esphome + +#endif // !defined(USE_RP2040) && !defined(USE_HOST) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 0b1d39575d..3fb217a24e 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -400,5 +400,4 @@ async def fan_is_on_off_to_code(config, condition_id, template_arg, args): @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_FAN") cg.add_global(fan_ns.using) diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 2ef6f8b7cc..48509e5705 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -4,6 +4,13 @@ #pragma once namespace esphome { + +#ifdef USE_API +namespace api { +class APIConnection; +} // namespace api +#endif + namespace fan { class FanTraits { @@ -36,6 +43,15 @@ class FanTraits { bool supports_preset_modes() const { return !this->preset_modes_.empty(); } protected: +#ifdef USE_API + // The API connection is a friend class to access internal methods + friend class api::APIConnection; + // This method returns a reference to the internal preset modes set. + // It is used by the API to avoid copying data when encoding messages. + // Warning: Do not use this method outside of the API connection code. + // It returns a reference to internal data that can be invalidated. + const std::set &supported_preset_modes_for_api_() const { return this->preset_modes_; } +#endif bool oscillation_{false}; bool speed_{false}; bool direction_{false}; diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp index bca7de811a..b3946a34b5 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -9,7 +9,6 @@ namespace fastled_base { static const char *const TAG = "fastled"; void FastLEDLightOutput::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->controller_->init(); this->controller_->setLeds(this->leds_, this->num_leds_); this->effect_data_ = new uint8_t[this->num_leds_]; // NOLINT diff --git a/esphome/components/fastled_spi/light.py b/esphome/components/fastled_spi/light.py index 81d8c3b32a..e863d33846 100644 --- a/esphome/components/fastled_spi/light.py +++ b/esphome/components/fastled_spi/light.py @@ -55,9 +55,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = await fastled_base.new_fastled_light(config) - rgb_order = cg.RawExpression( - config[CONF_RGB_ORDER] if CONF_RGB_ORDER in config else "RGB" - ) + rgb_order = cg.RawExpression(config.get(CONF_RGB_ORDER, "RGB")) data_rate = None if CONF_DATA_RATE in config: diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index e28548428c..54a267a404 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -57,8 +57,6 @@ void FingerprintGrowComponent::update() { } void FingerprintGrowComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->has_sensing_pin_ = (this->sensing_pin_ != nullptr); this->has_power_pin_ = (this->sensor_power_pin_ != nullptr); diff --git a/esphome/components/fs3000/fs3000.cpp b/esphome/components/fs3000/fs3000.cpp index c99772a23d..cea599211d 100644 --- a/esphome/components/fs3000/fs3000.cpp +++ b/esphome/components/fs3000/fs3000.cpp @@ -7,8 +7,6 @@ namespace fs3000 { static const char *const TAG = "fs3000"; void FS3000Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - if (model_ == FIVE) { // datasheet gives 9 points to interpolate from for the 1005 model static const uint16_t RAW_DATA_POINTS_1005[9] = {409, 915, 1522, 2066, 2523, 2908, 3256, 3572, 3686}; diff --git a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp index 9873a88fde..505c3cffc0 100644 --- a/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp +++ b/esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.cpp @@ -9,7 +9,6 @@ namespace ft5x06 { static const char *const TAG = "ft5x06.touchscreen"; void FT5x06Touchscreen::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (this->interrupt_pin_ != nullptr) { this->interrupt_pin_->setup(); this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); @@ -50,7 +49,6 @@ void FT5x06Touchscreen::continue_setup_() { this->y_raw_max_ = this->display_->get_native_height(); } } - ESP_LOGCONFIG(TAG, "FT5x06 Touchscreen setup complete"); } void FT5x06Touchscreen::update_touches() { diff --git a/esphome/components/ft63x6/ft63x6.cpp b/esphome/components/ft63x6/ft63x6.cpp index ba5b2094a5..f7c4f255a0 100644 --- a/esphome/components/ft63x6/ft63x6.cpp +++ b/esphome/components/ft63x6/ft63x6.cpp @@ -28,7 +28,6 @@ static const uint8_t FT63X6_ADDR_CHIP_ID = 0xA3; static const char *const TAG = "FT63X6"; void FT63X6Touchscreen::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (this->interrupt_pin_ != nullptr) { this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index e8401aa09b..096b06917a 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -34,7 +34,6 @@ void GDK101Component::update() { void GDK101Component::setup() { uint8_t data[2]; - ESP_LOGCONFIG(TAG, "Running setup"); // first, reset the sensor if (!this->reset_sensor_(data)) { this->status_set_error("Reset failed!"); diff --git a/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp index 5a24c63525..e2a64b6877 100644 --- a/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp +++ b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp @@ -17,7 +17,6 @@ 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!"); diff --git a/esphome/components/gl_r01_i2c/sensor.py b/esphome/components/gl_r01_i2c/sensor.py index 9f6f75faf7..58db72540e 100644 --- a/esphome/components/gl_r01_i2c/sensor.py +++ b/esphome/components/gl_r01_i2c/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import i2c, sensor +import esphome.config_validation as cv from esphome.const import ( CONF_ID, DEVICE_CLASS_DISTANCE, diff --git a/esphome/components/gpio/one_wire/gpio_one_wire.cpp b/esphome/components/gpio/one_wire/gpio_one_wire.cpp index ee80fde6fa..4191c45de1 100644 --- a/esphome/components/gpio/one_wire/gpio_one_wire.cpp +++ b/esphome/components/gpio/one_wire/gpio_one_wire.cpp @@ -8,7 +8,6 @@ namespace gpio { static const char *const TAG = "gpio.one_wire"; void GPIOOneWireBus::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->t_pin_->setup(); this->t_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); // clear bus with 480µs high, otherwise initial reset in search might fail diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index 6f901d602d..b67af5e95d 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -8,8 +8,6 @@ static const char *const TAG = "switch.gpio"; float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } void GPIOSwitch::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); - bool initial_state = this->get_initial_state_with_restore_mode().value_or(false); // write state before setup diff --git a/esphome/components/gpio_expander/cached_gpio.h b/esphome/components/gpio_expander/cached_gpio.h index 78c675cdb2..d7230eb0b3 100644 --- a/esphome/components/gpio_expander/cached_gpio.h +++ b/esphome/components/gpio_expander/cached_gpio.h @@ -2,10 +2,11 @@ #include #include +#include +#include #include "esphome/core/hal.h" -namespace esphome { -namespace gpio_expander { +namespace esphome::gpio_expander { /// @brief A class to cache the read state of a GPIO expander. /// This class caches reads between GPIO Pins which are on the same bank. @@ -17,12 +18,22 @@ namespace gpio_expander { /// N - Number of pins template class CachedGpioExpander { public: + /// @brief Read the state of the given pin. This will invalidate the cache for the given pin number. + /// @param pin Pin number to read + /// @return Pin state bool digital_read(T pin) { - uint8_t bank = pin / (sizeof(T) * BITS_PER_BYTE); - if (this->read_cache_invalidated_[bank]) { - this->read_cache_invalidated_[bank] = false; + const uint8_t bank = pin / BANK_SIZE; + const T pin_mask = (1 << (pin % BANK_SIZE)); + // Check if specific pin cache is valid + if (this->read_cache_valid_[bank] & pin_mask) { + // Invalidate pin + this->read_cache_valid_[bank] &= ~pin_mask; + } else { + // Read whole bank from hardware if (!this->digital_read_hw(pin)) return false; + // Mark bank cache as valid except the pin that is being returned now + this->read_cache_valid_[bank] = std::numeric_limits::max() & ~pin_mask; } return this->digital_read_cache(pin); } @@ -36,18 +47,16 @@ template class CachedGpioExpander { virtual bool digital_read_cache(T pin) = 0; /// @brief Call component low level function to write GPIO state to device virtual void digital_write_hw(T pin, bool value) = 0; - const uint8_t cache_byte_size_ = N / (sizeof(T) * BITS_PER_BYTE); /// @brief Invalidate cache. This function should be called in component loop(). - void reset_pin_cache_() { - for (T i = 0; i < this->cache_byte_size_; i++) { - this->read_cache_invalidated_[i] = true; - } - } + void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); } - static const uint8_t BITS_PER_BYTE = 8; - std::array read_cache_invalidated_{}; + static constexpr uint8_t BITS_PER_BYTE = 8; + static constexpr uint8_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE; + static constexpr size_t BANKS = N / BANK_SIZE; + static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T); + + T read_cache_valid_[BANKS]{0}; }; -} // namespace gpio_expander -} // namespace esphome +} // namespace esphome::gpio_expander diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 7ccd82dec3..a872cf7015 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -84,7 +84,6 @@ CONFIG_SCHEMA = cv.All( ) .extend(cv.polling_component_schema("20s")) .extend(uart.UART_DEVICE_SCHEMA), - cv.only_with_arduino, ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("gps", require_rx=True) @@ -123,4 +122,9 @@ async def to_code(config): cg.add(var.set_hdop_sensor(sens)) # https://platformio.org/lib/show/1655/TinyGPSPlus - cg.add_library("mikalhart/TinyGPSPlus", "1.1.0") + # Using fork of TinyGPSPlus patched to build on ESP-IDF + cg.add_library( + "TinyGPSPlus", + None, + "https://github.com/esphome/TinyGPSPlus.git#v1.1.0", + ) diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index 9dcb351b39..65cddcd984 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "gps.h" #include "esphome/core/log.h" @@ -22,73 +20,76 @@ void GPS::dump_config() { } void GPS::update() { - if (this->latitude_sensor_ != nullptr) + if (this->latitude_sensor_ != nullptr) { this->latitude_sensor_->publish_state(this->latitude_); + } - if (this->longitude_sensor_ != nullptr) + if (this->longitude_sensor_ != nullptr) { this->longitude_sensor_->publish_state(this->longitude_); + } - if (this->speed_sensor_ != nullptr) + if (this->speed_sensor_ != nullptr) { this->speed_sensor_->publish_state(this->speed_); + } - if (this->course_sensor_ != nullptr) + if (this->course_sensor_ != nullptr) { this->course_sensor_->publish_state(this->course_); + } - if (this->altitude_sensor_ != nullptr) + if (this->altitude_sensor_ != nullptr) { this->altitude_sensor_->publish_state(this->altitude_); + } - if (this->satellites_sensor_ != nullptr) + if (this->satellites_sensor_ != nullptr) { this->satellites_sensor_->publish_state(this->satellites_); + } - if (this->hdop_sensor_ != nullptr) + if (this->hdop_sensor_ != nullptr) { this->hdop_sensor_->publish_state(this->hdop_); + } } void GPS::loop() { while (this->available() > 0 && !this->has_time_) { - if (this->tiny_gps_.encode(this->read())) { - if (this->tiny_gps_.location.isUpdated()) { - this->latitude_ = this->tiny_gps_.location.lat(); - this->longitude_ = this->tiny_gps_.location.lng(); + if (!this->tiny_gps_.encode(this->read())) { + continue; + } + if (this->tiny_gps_.location.isUpdated()) { + this->latitude_ = this->tiny_gps_.location.lat(); + this->longitude_ = this->tiny_gps_.location.lng(); + ESP_LOGV(TAG, "Latitude, Longitude: %.6f°, %.6f°", this->latitude_, this->longitude_); + } - ESP_LOGD(TAG, "Location:"); - ESP_LOGD(TAG, " Lat: %.6f °", this->latitude_); - ESP_LOGD(TAG, " Lon: %.6f °", this->longitude_); - } + if (this->tiny_gps_.speed.isUpdated()) { + this->speed_ = this->tiny_gps_.speed.kmph(); + ESP_LOGV(TAG, "Speed: %.3f km/h", this->speed_); + } - if (this->tiny_gps_.speed.isUpdated()) { - this->speed_ = this->tiny_gps_.speed.kmph(); - ESP_LOGD(TAG, "Speed: %.3f km/h", this->speed_); - } + if (this->tiny_gps_.course.isUpdated()) { + this->course_ = this->tiny_gps_.course.deg(); + ESP_LOGV(TAG, "Course: %.2f°", this->course_); + } - if (this->tiny_gps_.course.isUpdated()) { - this->course_ = this->tiny_gps_.course.deg(); - ESP_LOGD(TAG, "Course: %.2f °", this->course_); - } + if (this->tiny_gps_.altitude.isUpdated()) { + this->altitude_ = this->tiny_gps_.altitude.meters(); + ESP_LOGV(TAG, "Altitude: %.2f m", this->altitude_); + } - if (this->tiny_gps_.altitude.isUpdated()) { - this->altitude_ = this->tiny_gps_.altitude.meters(); - ESP_LOGD(TAG, "Altitude: %.2f m", this->altitude_); - } + if (this->tiny_gps_.satellites.isUpdated()) { + this->satellites_ = this->tiny_gps_.satellites.value(); + ESP_LOGV(TAG, "Satellites: %d", this->satellites_); + } - if (this->tiny_gps_.satellites.isUpdated()) { - this->satellites_ = this->tiny_gps_.satellites.value(); - ESP_LOGD(TAG, "Satellites: %d", this->satellites_); - } + if (this->tiny_gps_.hdop.isUpdated()) { + this->hdop_ = this->tiny_gps_.hdop.hdop(); + ESP_LOGV(TAG, "HDOP: %.3f", this->hdop_); + } - if (this->tiny_gps_.hdop.isUpdated()) { - this->hdop_ = this->tiny_gps_.hdop.hdop(); - ESP_LOGD(TAG, "HDOP: %.3f", this->hdop_); - } - - for (auto *listener : this->listeners_) { - listener->on_update(this->tiny_gps_); - } + for (auto *listener : this->listeners_) { + listener->on_update(this->tiny_gps_); } } } } // namespace gps } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h index 7bc23ed1e0..36923c68be 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -1,10 +1,8 @@ #pragma once -#ifdef USE_ARDUINO - -#include "esphome/core/component.h" -#include "esphome/components/uart/uart.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/component.h" #include #include @@ -53,8 +51,9 @@ class GPS : public PollingComponent, public uart::UARTDevice { float speed_{NAN}; float course_{NAN}; float altitude_{NAN}; - uint16_t satellites_{0}; float hdop_{NAN}; + uint16_t satellites_{0}; + bool has_time_{false}; sensor::Sensor *latitude_sensor_{nullptr}; sensor::Sensor *longitude_sensor_{nullptr}; @@ -64,12 +63,9 @@ class GPS : public PollingComponent, public uart::UARTDevice { sensor::Sensor *satellites_sensor_{nullptr}; sensor::Sensor *hdop_sensor_{nullptr}; - bool has_time_{false}; TinyGPSPlus tiny_gps_; std::vector listeners_{}; }; } // namespace gps } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index 0f1b989f77..cff8c1fb07 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "gps_time.h" #include "esphome/core/log.h" @@ -9,12 +7,10 @@ namespace gps { static const char *const TAG = "gps.time"; void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { - if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid()) - return; - if (!tiny_gps.time.isUpdated() || !tiny_gps.date.isUpdated()) - return; - if (tiny_gps.date.year() < 2019) + if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid() || !tiny_gps.time.isUpdated() || + !tiny_gps.date.isUpdated() || tiny_gps.date.year() < 2025) { return; + } ESPTime val{}; val.year = tiny_gps.date.year(); @@ -34,5 +30,3 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { } // namespace gps } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/gps/time/gps_time.h b/esphome/components/gps/time/gps_time.h index d0d1db83b5..a8414f0015 100644 --- a/esphome/components/gps/time/gps_time.h +++ b/esphome/components/gps/time/gps_time.h @@ -1,10 +1,8 @@ #pragma once -#ifdef USE_ARDUINO - -#include "esphome/core/component.h" -#include "esphome/components/time/real_time_clock.h" #include "esphome/components/gps/gps.h" +#include "esphome/components/time/real_time_clock.h" +#include "esphome/core/component.h" namespace esphome { namespace gps { @@ -13,8 +11,9 @@ class GPSTime : public time::RealTimeClock, public GPSListener { public: void update() override { this->from_tiny_gps_(this->get_tiny_gps()); }; void on_update(TinyGPSPlus &tiny_gps) override { - if (!this->has_time_) + if (!this->has_time_) { this->from_tiny_gps_(tiny_gps); + } } protected: @@ -24,5 +23,3 @@ class GPSTime : public time::RealTimeClock, public GPSListener { } // namespace gps } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/graph/__init__.py b/esphome/components/graph/__init__.py index 6e8ba44bec..d72fe40dd2 100644 --- a/esphome/components/graph/__init__.py +++ b/esphome/components/graph/__init__.py @@ -116,7 +116,7 @@ GRAPH_SCHEMA = cv.Schema( def _relocate_fields_to_subfolder(config, subfolder, subschema): - fields = [k.schema for k in subschema.schema.keys()] + fields = [k.schema for k in subschema.schema] fields.remove(CONF_ID) if subfolder in config: # Ensure no ambiguous fields in base of config diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp index 361f3e04fd..4842ee5d06 100644 --- a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp @@ -33,7 +33,6 @@ bool GroveGasMultichannelV2Component::read_sensor_(uint8_t address, sensor::Sens } void GroveGasMultichannelV2Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // Before reading sensor values, must preheat sensor if (!(this->write_bytes(GROVE_GAS_MC_V2_HEAT_ON, {}))) { this->mark_failed(); diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp index 0dfb8478e7..a249984647 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -24,7 +24,6 @@ void GroveMotorDriveTB6612FNG::dump_config() { } void GroveMotorDriveTB6612FNG::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (!this->standby()) { this->mark_failed(); return; diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 5c540effd0..07218843dd 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -20,13 +20,11 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned #define ERROR_CHECK(err) \ if ((err) != i2c::ERROR_OK) { \ - this->status_set_warning("Communication failure"); \ + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); \ return; \ } void GT911Touchscreen::setup() { - i2c::ErrorCode err; - ESP_LOGCONFIG(TAG, "Running setup"); if (this->reset_pin_ != nullptr) { this->reset_pin_->setup(); this->reset_pin_->digital_write(false); @@ -36,9 +34,14 @@ void GT911Touchscreen::setup() { this->interrupt_pin_->digital_write(false); } delay(2); - this->reset_pin_->digital_write(true); - delay(50); // NOLINT + this->reset_pin_->digital_write(true); // wait 50ms after reset + this->set_timeout(50, [this] { this->setup_internal_(); }); + return; } + this->setup_internal_(); +} + +void GT911Touchscreen::setup_internal_() { if (this->interrupt_pin_ != nullptr) { // set pre-configured input mode this->interrupt_pin_->setup(); @@ -46,7 +49,7 @@ void GT911Touchscreen::setup() { // check the configuration of the int line. uint8_t data[4]; - err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); + i2c::ErrorCode err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) { this->address_ = SECONDARY_ADDRESS; err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); @@ -54,7 +57,7 @@ void GT911Touchscreen::setup() { if (err == i2c::ERROR_OK) { err = this->read(data, 1); if (err == i2c::ERROR_OK) { - ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]); + ESP_LOGD(TAG, "Switches ADDR: 0x%02X DATA: 0x%02X", this->address_, data[0]); if (this->interrupt_pin_ != nullptr) { this->attach_interrupt_(this->interrupt_pin_, (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); @@ -76,18 +79,24 @@ void GT911Touchscreen::setup() { } } if (err != i2c::ERROR_OK) { - this->mark_failed("Failed to read calibration"); + this->mark_failed("Calibration error"); return; } } - if (err != i2c::ERROR_OK) { - this->mark_failed("Failed to communicate"); - } - ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete"); + if (err != i2c::ERROR_OK) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return; + } + this->setup_done_ = true; } void GT911Touchscreen::update_touches() { + this->skip_update_ = true; // skip send touch events by default, set to false after successful error checks + if (!this->setup_done_) { + return; + } + i2c::ErrorCode err; uint8_t touch_state = 0; uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte @@ -100,7 +109,6 @@ void GT911Touchscreen::update_touches() { uint8_t num_of_touches = touch_state & 0x07; if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) { - this->skip_update_ = true; // skip send touch events, touchscreen is not ready yet. return; } @@ -110,6 +118,7 @@ void GT911Touchscreen::update_touches() { err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1); ERROR_CHECK(err); + this->skip_update_ = false; // All error checks passed, send touch events for (uint8_t i = 0; i != num_of_touches; i++) { uint16_t id = data[i][0]; uint16_t x = encode_uint16(data[i][2], data[i][1]); diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h index 17636a2ada..85025b5522 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.h +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -15,8 +15,20 @@ class GT911ButtonListener { class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { public: + /// @brief Initialize the GT911 touchscreen. + /// + /// If @ref reset_pin_ is set, the touchscreen will be hardware reset, + /// and the rest of the setup will be scheduled to run 50ms later using @ref set_timeout() + /// to allow the device to stabilize after reset. + /// + /// If @ref interrupt_pin_ is set, it will be temporarily configured during reset + /// to control I2C address selection. + /// + /// After the timeout, or immediately if no reset is performed, @ref setup_internal_() + /// is called to complete the initialization. void setup() override; void dump_config() override; + bool can_proceed() override { return this->setup_done_; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } @@ -25,8 +37,20 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice protected: void update_touches() override; - InternalGPIOPin *interrupt_pin_{}; - GPIOPin *reset_pin_{}; + /// @brief Perform the internal setup routine for the GT911 touchscreen. + /// + /// This function checks the I2C address, configures the interrupt pin (if available), + /// reads the touchscreen mode from the controller, and attempts to read calibration + /// data (maximum X and Y values) if not already set. + /// + /// On success, sets @ref setup_done_ to true. + /// On failure, calls @ref mark_failed() with an appropriate error message. + void setup_internal_(); + /// @brief True if the touchscreen setup has completed successfully. + bool setup_done_{false}; + + InternalGPIOPin *interrupt_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; std::vector button_listeners_; uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update. }; diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 0393c263d4..8c3649058f 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -330,8 +330,7 @@ HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id( ) async def display_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( @@ -342,8 +341,7 @@ async def display_action_to_code(config, action_id, template_arg, args): ) async def beeper_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) # Start self cleaning or steri-cleaning action action @@ -359,8 +357,7 @@ async def beeper_action_to_code(config, action_id, template_arg, args): ) async def start_cleaning_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) # Set vertical airflow direction action @@ -417,8 +414,7 @@ async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, ) async def health_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( @@ -432,8 +428,7 @@ async def health_action_to_code(config, action_id, template_arg, args): ) async def power_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) def _final_validate(config): diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index a784accdf4..4f933b08e3 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -242,7 +242,6 @@ haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(haier_pr } void HaierClimateBase::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // Set timestamp here to give AC time to boot this->last_request_timestamp_ = std::chrono::steady_clock::now(); this->set_phase(ProtocolPhases::SENDING_INIT_1); diff --git a/esphome/components/hbridge/switch/hbridge_switch.cpp b/esphome/components/hbridge/switch/hbridge_switch.cpp index 2a1afa48c5..55012fed21 100644 --- a/esphome/components/hbridge/switch/hbridge_switch.cpp +++ b/esphome/components/hbridge/switch/hbridge_switch.cpp @@ -10,8 +10,6 @@ static const char *const TAG = "switch.hbridge"; float HBridgeSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } void HBridgeSwitch::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); - optional initial_state = this->get_initial_state_with_restore_mode(); // Like GPIOSwitch does, set the pin state both before and after pin setup() diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index 956d01ed82..6d16133c36 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -13,8 +13,6 @@ static const uint8_t HDC1080_CMD_TEMPERATURE = 0x00; static const uint8_t HDC1080_CMD_HUMIDITY = 0x01; void HDC1080Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - const uint8_t data[2] = { 0b00000000, // resolution 14bit for both humidity and temperature 0b00000000 // reserved diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 0f9f146ae9..4f83bf2435 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -126,6 +126,6 @@ async def to_code(config): cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - cg.add_library("tonia/HeatpumpIR", "1.0.35") - if CORE.is_libretiny: + cg.add_library("tonia/HeatpumpIR", "1.0.37") + if CORE.is_libretiny or CORE.is_esp32: CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index ea1d081790..a28678e630 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -11,7 +11,6 @@ static const uint32_t HLW8012_CLOCK_FREQUENCY = 3579000; void HLW8012Component::setup() { float reference_voltage = 0; - ESP_LOGCONFIG(TAG, "Running setup"); this->sel_pin_->setup(); this->sel_pin_->digital_write(this->current_mode_); this->cf_store_.pulse_counter_setup(this->cf_pin_); diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index b165e361ff..a19d9dd09f 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -11,7 +11,6 @@ static const uint8_t PM_2_5_VALUE_INDEX = 6; static const uint8_t PM_10_0_VALUE_INDEX = 7; void HM3301Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (i2c::ERROR_OK != this->write(&SELECT_COMM_CMD, 1)) { error_code_ = ERROR_COMM; this->mark_failed(); diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index fe90b25af2..101493ad91 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -22,7 +22,6 @@ static const uint8_t HMC5883L_REGISTER_IDENTIFICATION_B = 0x0B; static const uint8_t HMC5883L_REGISTER_IDENTIFICATION_C = 0x0C; void HMC5883LComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t id[3]; if (!this->read_byte(HMC5883L_REGISTER_IDENTIFICATION_A, &id[0]) || !this->read_byte(HMC5883L_REGISTER_IDENTIFICATION_B, &id[1]) || diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index 223d6c18c3..7b23775b47 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -38,3 +38,4 @@ def setup_home_assistant_entity(var, config): cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) if CONF_ATTRIBUTE in config: cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) + cg.add_define("USE_API_HOMEASSISTANT_STATES") diff --git a/esphome/components/homeassistant/number/__init__.py b/esphome/components/homeassistant/number/__init__.py index a6cc615a64..8f760772c3 100644 --- a/esphome/components/homeassistant/number/__init__.py +++ b/esphome/components/homeassistant/number/__init__.py @@ -23,6 +23,7 @@ CONFIG_SCHEMA = ( async def to_code(config): + cg.add_define("USE_API_HOMEASSISTANT_SERVICES") var = await number.new_number( config, min_value=0, diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index a7f71c3244..87bf6727f2 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -83,18 +83,22 @@ void HomeassistantNumber::control(float value) { this->publish_state(value); + static constexpr auto SERVICE_NAME = StringRef::from_lit("number.set_value"); + static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id"); + static constexpr auto VALUE_KEY = StringRef::from_lit("value"); + api::HomeassistantServiceResponse resp; - resp.service = "number.set_value"; + resp.set_service(SERVICE_NAME); - api::HomeassistantServiceMap entity_id; - entity_id.key = "entity_id"; + resp.data.emplace_back(); + auto &entity_id = resp.data.back(); + entity_id.set_key(ENTITY_ID_KEY); entity_id.value = this->entity_id_; - resp.data.push_back(entity_id); - api::HomeassistantServiceMap entity_value; - entity_value.key = "value"; + resp.data.emplace_back(); + auto &entity_value = resp.data.back(); + entity_value.set_key(VALUE_KEY); entity_value.value = to_string(value); - resp.data.push_back(entity_value); api::global_api_server->send_homeassistant_service_call(resp); } diff --git a/esphome/components/homeassistant/switch/__init__.py b/esphome/components/homeassistant/switch/__init__.py index 384f82bbad..c299a731f2 100644 --- a/esphome/components/homeassistant/switch/__init__.py +++ b/esphome/components/homeassistant/switch/__init__.py @@ -37,6 +37,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + cg.add_define("USE_API_HOMEASSISTANT_SERVICES") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await switch.register_switch(var, config) diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.cpp b/esphome/components/homeassistant/switch/homeassistant_switch.cpp index 0451c95069..b3300335b9 100644 --- a/esphome/components/homeassistant/switch/homeassistant_switch.cpp +++ b/esphome/components/homeassistant/switch/homeassistant_switch.cpp @@ -40,17 +40,21 @@ void HomeassistantSwitch::write_state(bool state) { return; } + static constexpr auto SERVICE_ON = StringRef::from_lit("homeassistant.turn_on"); + static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off"); + static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id"); + api::HomeassistantServiceResponse resp; if (state) { - resp.service = "homeassistant.turn_on"; + resp.set_service(SERVICE_ON); } else { - resp.service = "homeassistant.turn_off"; + resp.set_service(SERVICE_OFF); } - api::HomeassistantServiceMap entity_id_kv; - entity_id_kv.key = "entity_id"; + resp.data.emplace_back(); + auto &entity_id_kv = resp.data.back(); + entity_id_kv.set_key(ENTITY_ID_KEY); entity_id_kv.value = this->entity_id_; - resp.data.push_back(entity_id_kv); api::global_api_server->send_homeassistant_service_call(resp); } diff --git a/esphome/components/honeywellabp/honeywellabp.cpp b/esphome/components/honeywellabp/honeywellabp.cpp index 9252e613dd..4c00f034aa 100644 --- a/esphome/components/honeywellabp/honeywellabp.cpp +++ b/esphome/components/honeywellabp/honeywellabp.cpp @@ -9,10 +9,7 @@ static const char *const TAG = "honeywellabp"; const float MIN_COUNT = 1638.4; // 1638 counts (10% of 2^14 counts or 0x0666) const float MAX_COUNT = 14745.6; // 14745 counts (90% of 2^14 counts or 0x3999) -void HONEYWELLABPSensor::setup() { - ESP_LOGD(TAG, "Setting up Honeywell ABP Sensor "); - this->spi_setup(); -} +void HONEYWELLABPSensor::setup() { this->spi_setup(); } uint8_t HONEYWELLABPSensor::readsensor_() { // Polls the sensor for new data. diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index a67d73fbb7..ba05e497c8 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, + ThreadModel, ) from esphome.core import CORE @@ -43,6 +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(ThreadModel.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/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index 0f97c67f9e..75770ceffe 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -8,7 +8,6 @@ namespace hte501 { static const char *const TAG = "hte501"; void HTE501Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t address[] = {0x70, 0x29}; this->write(address, 2, false); uint8_t identification[9]; diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 0d32bc97c2..146458f53b 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -70,9 +70,8 @@ def validate_url(value): def validate_ssl_verification(config): error_message = "" - if CORE.is_esp32: - if not CORE.using_esp_idf and config[CONF_VERIFY_SSL]: - error_message = "ESPHome supports certificate verification only via ESP-IDF" + if CORE.is_esp32 and not CORE.using_esp_idf and config[CONF_VERIFY_SSL]: + error_message = "ESPHome supports certificate verification only via ESP-IDF" if CORE.is_rp2040 and config[CONF_VERIFY_SSL]: error_message = "ESPHome does not support certificate verification on RP2040" diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 68c06d28f2..89a0891b03 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -157,8 +157,8 @@ std::shared_ptr HttpRequestIDF::perform(std::string url, std::str container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); container->set_response_headers(user_data.response_headers); + container->duration_ms = millis() - start; if (is_success(container->status_code)) { - container->duration_ms = millis() - start; return container; } @@ -191,8 +191,8 @@ std::shared_ptr HttpRequestIDF::perform(std::string url, std::str container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); + container->duration_ms = millis() - start; if (is_success(container->status_code)) { - container->duration_ms = millis() - start; return container; } diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index b5d6ad45d5..f2e7ae93cb 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -18,8 +18,6 @@ static const uint8_t HTU21D_READHEATER_REG_CMD = 0x11; /**< Read Heater Control static const uint8_t HTU21D_REG_HTRE_BIT = 0x02; /**< Control Register Heater Bit */ void HTU21DComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - if (!this->write_bytes(HTU21D_REGISTER_RESET, nullptr, 0)) { this->mark_failed(); return; diff --git a/esphome/components/htu31d/htu31d.cpp b/esphome/components/htu31d/htu31d.cpp index 284548ed96..562078aacb 100644 --- a/esphome/components/htu31d/htu31d.cpp +++ b/esphome/components/htu31d/htu31d.cpp @@ -75,8 +75,6 @@ uint8_t compute_crc(uint32_t value) { * I2C. */ void HTU31DComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - if (!this->reset_()) { this->mark_failed(); return; diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index 0fc8b29604..67ec4549df 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -8,7 +8,6 @@ namespace hx711 { static const char *const TAG = "hx711"; void HX711Sensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); this->sck_pin_->setup(); this->dout_pin_->setup(); this->sck_pin_->digital_write(false); diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 9d4680fdf4..4872d68610 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -41,7 +41,6 @@ void HydreonRGxxComponent::dump_config() { } void HydreonRGxxComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); while (this->available() != 0) { this->read(); } diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index 6c9f1e2877..f270b72e24 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -66,11 +66,10 @@ PROTOCOL_NAMES = { def _validate(config): for conf, models in SUPPORTED_OPTIONS.items(): - if conf in config: - if config[CONF_MODEL] not in models: - raise cv.Invalid( - f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}" - ) + if conf in config and config[CONF_MODEL] not in models: + raise cv.Invalid( + f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}" + ) return config diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 5c1e15d814..da94aa940d 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -94,7 +94,7 @@ class I2CBus { protected: /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair /// that contains the address and the corresponding bool presence flag. - void i2c_scan_() { + virtual void i2c_scan() { for (uint8_t address = 8; address < 120; address++) { auto err = writev(address, nullptr, 0); if (err == ERROR_OK) { diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index a85df0a4cd..24385745eb 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -13,7 +13,6 @@ namespace i2c { static const char *const TAG = "i2c.arduino"; void ArduinoI2CBus::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); recover_(); #if defined(USE_ESP32) @@ -42,7 +41,7 @@ void ArduinoI2CBus::setup() { this->initialized_ = true; if (this->scan_) { ESP_LOGV(TAG, "Scanning bus for active devices"); - this->i2c_scan_(); + this->i2c_scan(); } } diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index e4643405ce..cf31ba1c0d 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP_IDF #include "i2c_bus_esp_idf.h" +#include #include #include #include "esphome/core/application.h" @@ -18,23 +19,73 @@ namespace i2c { 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 +104,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 +120,15 @@ void IDFI2CBus::setup() { this->mark_failed(); return; } + initialized_ = true; if (this->scan_) { ESP_LOGV(TAG, "Scanning bus for active devices"); - this->i2c_scan_(); + this->i2c_scan(); } +#endif } + void IDFI2CBus::dump_config() { ESP_LOGCONFIG(TAG, "I2C Bus:"); ESP_LOGCONFIG(TAG, @@ -101,13 +151,13 @@ void IDFI2CBus::dump_config() { break; } if (this->scan_) { - ESP_LOGI(TAG, "Results from bus scan:"); + ESP_LOGCONFIG(TAG, "Results from bus scan:"); if (scan_results_.empty()) { - ESP_LOGI(TAG, "Found no devices"); + ESP_LOGCONFIG(TAG, "Found no devices"); } else { for (const auto &s : scan_results_) { if (s.second) { - ESP_LOGI(TAG, "Found device at address 0x%02X", s.first); + ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first); } else { ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first); } @@ -116,6 +166,17 @@ void IDFI2CBus::dump_config() { } } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) +void IDFI2CBus::i2c_scan() { + for (uint8_t address = 8; address < 120; address++) { + auto err = i2c_master_probe(this->bus_, address, 20); + if (err == ESP_OK) { + this->scan_results_.emplace_back(address, true); + } + } +} +#endif + ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { // logging is only enabled with vv level, if warnings are shown the caller // should log them @@ -123,6 +184,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 +297,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 +315,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 +338,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 +426,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..4e8f86fd0c 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 "esp_idf_version.h" #include "esphome/core/component.h" #include "i2c_bus.h" +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) +#include +#else +#include +#endif namespace esphome { namespace i2c { @@ -38,6 +43,11 @@ 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_; + void i2c_scan() override; +#endif i2c_port_t port_; uint8_t sda_pin_; bool sda_pullup_enabled_; diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 9a2aa0362f..aa0a688fa0 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -1,6 +1,6 @@ from esphome import pins import esphome.codegen as cg -from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, @@ -243,10 +243,7 @@ def _final_validate(_): def use_legacy(): - if CORE.using_esp_idf: - if not _use_legacy_driver: - return False - return True + return not (CORE.using_esp_idf and not _use_legacy_driver) FINAL_VALIDATE_SCHEMA = _final_validate @@ -258,6 +255,10 @@ async def to_code(config): if use_legacy(): cg.add_define("USE_I2S_LEGACY") + # Helps avoid callbacks being skipped due to processor load + if CORE.using_esp_idf: + add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) + cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) if CONF_I2S_BCLK_PIN in config: cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) diff --git a/esphome/components/i2s_audio/i2s_audio.cpp b/esphome/components/i2s_audio/i2s_audio.cpp index 7f233516e6..43064498cc 100644 --- a/esphome/components/i2s_audio/i2s_audio.cpp +++ b/esphome/components/i2s_audio/i2s_audio.cpp @@ -10,8 +10,6 @@ namespace i2s_audio { static const char *const TAG = "i2s_audio"; void I2SAudioComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - static i2s_port_t next_port_num = I2S_NUM_0; if (next_port_num >= SOC_I2S_NUM) { ESP_LOGE(TAG, "Too many components"); diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp index 57e184d7f8..39301220d5 100644 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp +++ b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp @@ -119,10 +119,7 @@ void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) { this->volume = volume; } -void I2SAudioMediaPlayer::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->state = media_player::MEDIA_PLAYER_STATE_IDLE; -} +void I2SAudioMediaPlayer::setup() { this->state = media_player::MEDIA_PLAYER_STATE_IDLE; } void I2SAudioMediaPlayer::loop() { switch (this->i2s_state_) { diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 7bbb94f6e3..0f02ba6c3a 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -44,9 +44,8 @@ PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3] def _validate_esp32_variant(config): variant = esp32.get_esp32_variant() if config[CONF_ADC_TYPE] == "external": - if config[CONF_PDM]: - if variant not in PDM_VARIANTS: - raise cv.Invalid(f"{variant} does not support PDM") + if config[CONF_PDM] and variant not in PDM_VARIANTS: + raise cv.Invalid(f"{variant} does not support PDM") return config if config[CONF_ADC_TYPE] == "internal": if variant not in INTERNAL_ADC_VARIANTS: @@ -122,9 +121,8 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config): - if not use_legacy(): - if config[CONF_ADC_TYPE] == "internal": - raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.") + if not use_legacy() and config[CONF_ADC_TYPE] == "internal": + raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.") FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index f442c74c9f..cdebc214e2 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -37,7 +37,6 @@ enum MicrophoneEventGroupBits : uint32_t { }; void I2SAudioMicrophone::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); #ifdef USE_I2S_LEGACY #if SOC_I2S_SUPPORTS_ADC if (this->adc_) { diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index acfd535188..de272ba23d 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/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 1042a7ebee..7ae3ec8b3b 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -9,6 +9,7 @@ #endif #include "esphome/components/audio/audio.h" +#include "esphome/components/audio/audio_transfer_buffer.h" #include "esphome/core/application.h" #include "esphome/core/hal.h" @@ -19,72 +20,33 @@ namespace esphome { namespace i2s_audio { -static const uint8_t DMA_BUFFER_DURATION_MS = 15; +static const uint32_t DMA_BUFFER_DURATION_MS = 15; static const size_t DMA_BUFFERS_COUNT = 4; -static const size_t TASK_DELAY_MS = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT / 2; - static const size_t TASK_STACK_SIZE = 4096; -static const ssize_t TASK_PRIORITY = 23; +static const ssize_t TASK_PRIORITY = 19; static const size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1; static const char *const TAG = "i2s_audio.speaker"; enum SpeakerEventGroupBits : uint32_t { - COMMAND_START = (1 << 0), // starts the speaker task + COMMAND_START = (1 << 0), // indicates loop should start speaker task COMMAND_STOP = (1 << 1), // stops the speaker task COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the speaker task once all data has been written - STATE_STARTING = (1 << 10), - STATE_RUNNING = (1 << 11), - STATE_STOPPING = (1 << 12), - STATE_STOPPED = (1 << 13), - ERR_TASK_FAILED_TO_START = (1 << 14), - ERR_ESP_INVALID_STATE = (1 << 15), - ERR_ESP_NOT_SUPPORTED = (1 << 16), - ERR_ESP_INVALID_ARG = (1 << 17), - ERR_ESP_INVALID_SIZE = (1 << 18), + + TASK_STARTING = (1 << 10), + TASK_RUNNING = (1 << 11), + TASK_STOPPING = (1 << 12), + TASK_STOPPED = (1 << 13), + ERR_ESP_NO_MEM = (1 << 19), - ERR_ESP_FAIL = (1 << 20), - ALL_ERR_ESP_BITS = ERR_ESP_INVALID_STATE | ERR_ESP_NOT_SUPPORTED | ERR_ESP_INVALID_ARG | ERR_ESP_INVALID_SIZE | - ERR_ESP_NO_MEM | ERR_ESP_FAIL, + + WARN_DROPPED_EVENT = (1 << 20), + ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits }; -// Translates a SpeakerEventGroupBits ERR_ESP bit to the coressponding esp_err_t -static esp_err_t err_bit_to_esp_err(uint32_t bit) { - switch (bit) { - case SpeakerEventGroupBits::ERR_ESP_INVALID_STATE: - return ESP_ERR_INVALID_STATE; - case SpeakerEventGroupBits::ERR_ESP_INVALID_ARG: - return ESP_ERR_INVALID_ARG; - case SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE: - return ESP_ERR_INVALID_SIZE; - case SpeakerEventGroupBits::ERR_ESP_NO_MEM: - return ESP_ERR_NO_MEM; - case SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED: - return ESP_ERR_NOT_SUPPORTED; - default: - return ESP_FAIL; - } -} - -/// @brief Multiplies the input array of Q15 numbers by a Q15 constant factor -/// -/// Based on `dsps_mulc_s16_ansi` from the esp-dsp library: -/// https://github.com/espressif/esp-dsp/blob/master/modules/math/mulc/fixed/dsps_mulc_s16_ansi.c -/// (accessed on 2024-09-30). -/// @param input Array of Q15 numbers -/// @param output Array of Q15 numbers -/// @param len Length of array -/// @param c Q15 constant factor -static void q15_multiplication(const int16_t *input, int16_t *output, size_t len, int16_t c) { - for (int i = 0; i < len; i++) { - int32_t acc = (int32_t) input[i] * (int32_t) c; - output[i] = (int16_t) (acc >> 15); - } -} - // Lists the Q15 fixed point scaling factor for volume reduction. // Has 100 values representing silence and a reduction [49, 48.5, ... 0.5, 0] dB. // dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014) @@ -99,8 +61,6 @@ static const std::vector Q15_VOLUME_SCALING_FACTORS = { 19508, 20665, 21891, 23189, 24565, 26022, 27566, 29201, 30933, 32767}; void I2SAudioSpeaker::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->event_group_ = xEventGroupCreate(); if (this->event_group_ == nullptr) { @@ -132,51 +92,80 @@ void I2SAudioSpeaker::dump_config() { void I2SAudioSpeaker::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); - if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) { - ESP_LOGD(TAG, "Starting"); + if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) { this->state_ = speaker::STATE_STARTING; - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STARTING); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); } - if (event_group_bits & SpeakerEventGroupBits::STATE_RUNNING) { + + // Handle the task's state + if (event_group_bits & SpeakerEventGroupBits::TASK_STARTING) { + ESP_LOGD(TAG, "Starting"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING); + } + if (event_group_bits & SpeakerEventGroupBits::TASK_RUNNING) { ESP_LOGD(TAG, "Started"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING); this->state_ = speaker::STATE_RUNNING; - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_RUNNING); - this->status_clear_warning(); - this->status_clear_error(); } - if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPING) { + if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) { ESP_LOGD(TAG, "Stopping"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING); this->state_ = speaker::STATE_STOPPING; - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPING); } - if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPED) { - if (!this->task_created_) { - ESP_LOGD(TAG, "Stopped"); - this->state_ = speaker::STATE_STOPPED; - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); - this->speaker_task_handle_ = nullptr; - } + if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPED) { + ESP_LOGD(TAG, "Stopped"); + + vTaskDelete(this->speaker_task_handle_); + this->speaker_task_handle_ = nullptr; + + this->stop_i2s_driver_(); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); + this->status_clear_error(); + + this->state_ = speaker::STATE_STOPPED; } - if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) { - this->status_set_error("Failed to start task"); - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); + // Log any errors encounted by the task + if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) { + ESP_LOGE(TAG, "Not enough memory"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); } - if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) { - uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS; - ESP_LOGW(TAG, "Writing failed: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); - this->status_set_warning(); + // Warn if any playback timestamp events are dropped, which drastically reduces synced playback accuracy + if (event_group_bits & SpeakerEventGroupBits::WARN_DROPPED_EVENT) { + ESP_LOGW(TAG, "Event dropped, synchronized playback accuracy is reduced"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::WARN_DROPPED_EVENT); } - if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED) { - this->status_set_error("Failed to adjust bus to match incoming audio"); - ESP_LOGE(TAG, "Incompatible audio format: sample rate = %" PRIu32 ", channels = %u, bits per sample = %u", - this->audio_stream_info_.get_sample_rate(), this->audio_stream_info_.get_channels(), - this->audio_stream_info_.get_bits_per_sample()); - } + // Handle the speaker's state + switch (this->state_) { + case speaker::STATE_STARTING: + if (this->status_has_error()) { + break; + } - xEventGroupClearBits(this->event_group_, ALL_ERR_ESP_BITS); + if (this->start_i2s_driver_(this->audio_stream_info_) != ESP_OK) { + ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); + this->status_momentary_error("driver-faiure", 1000); + break; + } + + if (this->speaker_task_handle_ == nullptr) { + xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, + &this->speaker_task_handle_); + + if (this->speaker_task_handle_ == nullptr) { + ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); + this->status_momentary_error("task-failure", 1000); + this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt + } + } + break; + case speaker::STATE_RUNNING: // Intentional fallthrough + case speaker::STATE_STOPPING: // Intentional fallthrough + case speaker::STATE_STOPPED: + break; + } } void I2SAudioSpeaker::set_volume(float volume) { @@ -227,83 +216,76 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick this->start(); } - if ((this->state_ != speaker::STATE_RUNNING) || (this->audio_ring_buffer_.use_count() != 1)) { + if (this->state_ != speaker::STATE_RUNNING) { // Unable to write data to a running speaker, so delay the max amount of time so it can get ready vTaskDelay(ticks_to_wait); ticks_to_wait = 0; } size_t bytes_written = 0; - if ((this->state_ == speaker::STATE_RUNNING) && (this->audio_ring_buffer_.use_count() == 1)) { - // Only one owner of the ring buffer (the speaker task), so the ring buffer is allocated and no other components are - // attempting to write to it. - - // Temporarily share ownership of the ring buffer so it won't be deallocated while writing - std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_; - bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait); + if (this->state_ == speaker::STATE_RUNNING) { + std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_.lock(); + if (temp_ring_buffer.use_count() == 2) { + // Only the speaker task and this temp_ring_buffer own the ring buffer, so its safe to write to + bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait); + } } return bytes_written; } bool I2SAudioSpeaker::has_buffered_data() const { - if (this->audio_ring_buffer_ != nullptr) { - return this->audio_ring_buffer_->available() > 0; + if (this->audio_ring_buffer_.use_count() > 0) { + std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_.lock(); + return temp_ring_buffer->available() > 0; } return false; } void I2SAudioSpeaker::speaker_task(void *params) { I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params; - this_speaker->task_created_ = true; - uint32_t event_group_bits = - xEventGroupWaitBits(this_speaker->event_group_, - SpeakerEventGroupBits::COMMAND_START | SpeakerEventGroupBits::COMMAND_STOP | - SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY, // Bit message to read - pdTRUE, // Clear the bits on exit - pdFALSE, // Don't wait for all the bits, - portMAX_DELAY); // Block indefinitely until a bit is set - - if (event_group_bits & (SpeakerEventGroupBits::COMMAND_STOP | SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY)) { - // Received a stop signal before the task was requested to start - this_speaker->delete_task_(0); - } - - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STARTING); - - audio::AudioStreamInfo audio_stream_info = this_speaker->audio_stream_info_; + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STARTING); const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT; // Ensure ring buffer duration is at least the duration of all DMA buffers const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this_speaker->buffer_duration_ms_); // The DMA buffers may have more bits per sample, so calculate buffer sizes based in the input audio stream info - const size_t data_buffer_size = audio_stream_info.ms_to_bytes(dma_buffers_duration_ms); - const size_t ring_buffer_size = audio_stream_info.ms_to_bytes(ring_buffer_duration); + const size_t ring_buffer_size = this_speaker->current_stream_info_.ms_to_bytes(ring_buffer_duration); - const size_t single_dma_buffer_input_size = data_buffer_size / DMA_BUFFERS_COUNT; + const uint32_t frames_to_fill_single_dma_buffer = + this_speaker->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS); + const size_t bytes_to_fill_single_dma_buffer = + this_speaker->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer); - if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(data_buffer_size, ring_buffer_size))) { - // Failed to allocate buffers - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); - this_speaker->delete_task_(data_buffer_size); + bool successful_setup = false; + std::unique_ptr transfer_buffer = + audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer); + + if (transfer_buffer != nullptr) { + std::shared_ptr temp_ring_buffer = RingBuffer::create(ring_buffer_size); + if (temp_ring_buffer.use_count() == 1) { + transfer_buffer->set_source(temp_ring_buffer); + this_speaker->audio_ring_buffer_ = temp_ring_buffer; + successful_setup = true; + } } - if (!this_speaker->send_esp_err_to_event_group_(this_speaker->start_i2s_driver_(audio_stream_info))) { - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_RUNNING); - + if (!successful_setup) { + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); + } else { bool stop_gracefully = false; + bool tx_dma_underflow = true; + + uint32_t frames_written = 0; uint32_t last_data_received_time = millis(); - bool tx_dma_underflow = false; - this_speaker->accumulated_frames_written_ = 0; + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_RUNNING); - // Keep looping if paused, there is no timeout configured, or data was received more recently than the configured - // timeout while (this_speaker->pause_state_ || !this_speaker->timeout_.has_value() || (millis() - last_data_received_time) <= this_speaker->timeout_.value()) { - event_group_bits = xEventGroupGetBits(this_speaker->event_group_); + uint32_t event_group_bits = xEventGroupGetBits(this_speaker->event_group_); if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { xEventGroupClearBits(this_speaker->event_group_, SpeakerEventGroupBits::COMMAND_STOP); @@ -314,7 +296,7 @@ void I2SAudioSpeaker::speaker_task(void *params) { stop_gracefully = true; } - if (this_speaker->audio_stream_info_ != audio_stream_info) { + if (this_speaker->audio_stream_info_ != this_speaker->current_stream_info_) { // Audio stream info changed, stop the speaker task so it will restart with the proper settings. break; } @@ -326,36 +308,75 @@ void I2SAudioSpeaker::speaker_task(void *params) { } } #else - bool overflow; - while (xQueueReceive(this_speaker->i2s_event_queue_, &overflow, 0)) { - if (overflow) { + int64_t write_timestamp; + while (xQueueReceive(this_speaker->i2s_event_queue_, &write_timestamp, 0)) { + // Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes + // on the timing info via the audio_output_callback. + uint32_t frames_sent = frames_to_fill_single_dma_buffer; + if (frames_to_fill_single_dma_buffer > frames_written) { tx_dma_underflow = true; + frames_sent = frames_written; + const uint32_t frames_zeroed = frames_to_fill_single_dma_buffer - frames_written; + write_timestamp -= this_speaker->current_stream_info_.frames_to_microseconds(frames_zeroed); + } else { + tx_dma_underflow = false; + } + frames_written -= frames_sent; + if (frames_sent > 0) { + this_speaker->audio_output_callback_(frames_sent, write_timestamp); } } #endif if (this_speaker->pause_state_) { // Pause state is accessed atomically, so thread safe - // Delay so the task can yields, then skip transferring audio data - delay(TASK_DELAY_MS); + // Delay so the task yields, then skip transferring audio data + vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); continue; } - size_t bytes_read = this_speaker->audio_ring_buffer_->read((void *) this_speaker->data_buffer_, data_buffer_size, - pdMS_TO_TICKS(TASK_DELAY_MS)); + // Wait half the duration of the data already written to the DMA buffers for new audio data + // The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000 + const uint32_t read_delay = + (this_speaker->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2; + + uint8_t *new_data = transfer_buffer->get_buffer_end(); // track start of any newly copied bytes + size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay)); if (bytes_read > 0) { - if ((audio_stream_info.get_bits_per_sample() == 16) && (this_speaker->q15_volume_factor_ < INT16_MAX)) { - // Scale samples by the volume factor in place - q15_multiplication((int16_t *) this_speaker->data_buffer_, (int16_t *) this_speaker->data_buffer_, - bytes_read / sizeof(int16_t), this_speaker->q15_volume_factor_); + if (this_speaker->q15_volume_factor_ < INT16_MAX) { + // Apply the software volume adjustment by unpacking the sample into a Q31 fixed-point number, shifting it, + // multiplying by the volume factor, and packing the sample back into the original bytes per sample. + + const size_t bytes_per_sample = this_speaker->current_stream_info_.samples_to_bytes(1); + const uint32_t len = bytes_read / bytes_per_sample; + + // Use Q16 for samples with 1 or 2 bytes: shifted_sample * gain_factor is Q16 * Q15 -> Q31 + int32_t shift = 15; // Q31 -> Q16 + int32_t gain_factor = this_speaker->q15_volume_factor_; // Q15 + + if (bytes_per_sample >= 3) { + // Use Q23 for samples with 3 or 4 bytes: shifted_sample * gain_factor is Q23 * Q8 -> Q31 + + shift = 8; // Q31 -> Q23 + gain_factor >>= 7; // Q15 -> Q8 + } + + for (uint32_t i = 0; i < len; ++i) { + int32_t sample = + audio::unpack_audio_sample_to_q31(&new_data[i * bytes_per_sample], bytes_per_sample); // Q31 + sample >>= shift; + sample *= gain_factor; // Q31 + audio::pack_q31_as_audio_sample(sample, &new_data[i * bytes_per_sample], bytes_per_sample); + } } #ifdef USE_ESP32_VARIANT_ESP32 // For ESP32 8/16 bit mono mode samples need to be switched. - if (audio_stream_info.get_channels() == 1 && audio_stream_info.get_bits_per_sample() <= 16) { + if (this_speaker->current_stream_info_.get_channels() == 1 && + this_speaker->current_stream_info_.get_bits_per_sample() <= 16) { size_t len = bytes_read / sizeof(int16_t); - int16_t *tmp_buf = (int16_t *) this_speaker->data_buffer_; + int16_t *tmp_buf = (int16_t *) new_data; for (int i = 0; i < len; i += 2) { int16_t tmp = tmp_buf[i]; tmp_buf[i] = tmp_buf[i + 1]; @@ -363,62 +384,87 @@ void I2SAudioSpeaker::speaker_task(void *params) { } } #endif - // Write the audio data to a single DMA buffer at a time to reduce latency for the audio duration played - // callback. - const uint32_t batches = (bytes_read + single_dma_buffer_input_size - 1) / single_dma_buffer_input_size; + } - for (uint32_t i = 0; i < batches; ++i) { - size_t bytes_written = 0; - size_t bytes_to_write = std::min(single_dma_buffer_input_size, bytes_read); - -#ifdef USE_I2S_LEGACY - if (audio_stream_info.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) { - i2s_write(this_speaker->parent_->get_port(), this_speaker->data_buffer_ + i * single_dma_buffer_input_size, - bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5)); - } else if (audio_stream_info.get_bits_per_sample() < (uint8_t) this_speaker->bits_per_sample_) { - i2s_write_expand(this_speaker->parent_->get_port(), - this_speaker->data_buffer_ + i * single_dma_buffer_input_size, bytes_to_write, - audio_stream_info.get_bits_per_sample(), this_speaker->bits_per_sample_, &bytes_written, - pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5)); - } -#else - i2s_channel_write(this_speaker->tx_handle_, this_speaker->data_buffer_ + i * single_dma_buffer_input_size, - bytes_to_write, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS * 5)); -#endif - - int64_t now = esp_timer_get_time(); - - if (bytes_written != bytes_to_write) { - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE); - } - bytes_read -= bytes_written; - - this_speaker->audio_output_callback_(audio_stream_info.bytes_to_frames(bytes_written), - now + dma_buffers_duration_ms * 1000); - - tx_dma_underflow = false; - last_data_received_time = millis(); - } - } else { - // No data received + if (transfer_buffer->available() == 0) { if (stop_gracefully && tx_dma_underflow) { break; } + vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS / 2)); + } else { + size_t bytes_written = 0; +#ifdef USE_I2S_LEGACY + if (this_speaker->current_stream_info_.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) { + i2s_write(this_speaker->parent_->get_port(), transfer_buffer->get_buffer_start(), + transfer_buffer->available(), &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); + } else if (this_speaker->current_stream_info_.get_bits_per_sample() < + (uint8_t) this_speaker->bits_per_sample_) { + i2s_write_expand(this_speaker->parent_->get_port(), transfer_buffer->get_buffer_start(), + transfer_buffer->available(), this_speaker->current_stream_info_.get_bits_per_sample(), + this_speaker->bits_per_sample_, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS)); + } +#else + if (tx_dma_underflow) { + // Temporarily disable channel and callback to reset the I2S driver's internal DMA buffer queue so timing + // callbacks are accurate. Preload the data. + i2s_channel_disable(this_speaker->tx_handle_); + const i2s_event_callbacks_t callbacks = { + .on_sent = nullptr, + }; + + i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker); + i2s_channel_preload_data(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(), + transfer_buffer->available(), &bytes_written); + } else { + // Audio is already playing, use regular I2S write to add to the DMA buffers + i2s_channel_write(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(), + &bytes_written, DMA_BUFFER_DURATION_MS); + } +#endif + if (bytes_written > 0) { + last_data_received_time = millis(); + frames_written += this_speaker->current_stream_info_.bytes_to_frames(bytes_written); + transfer_buffer->decrease_buffer_length(bytes_written); + if (tx_dma_underflow) { + tx_dma_underflow = false; +#ifndef USE_I2S_LEGACY + // Reset the event queue timestamps + // Enable the on_sent callback to accurately track the timestamps of played audio + // Enable the I2S channel to start sending the preloaded audio + + xQueueReset(this_speaker->i2s_event_queue_); + + const i2s_event_callbacks_t callbacks = { + .on_sent = i2s_on_sent_cb, + }; + i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker); + + i2s_channel_enable(this_speaker->tx_handle_); +#endif + } +#ifdef USE_I2S_LEGACY + // The legacy driver doesn't easily support the callback approach for timestamps, so fall back to a direct but + // less accurate approach. + this_speaker->audio_output_callback_(this_speaker->current_stream_info_.bytes_to_frames(bytes_written), + esp_timer_get_time() + dma_buffers_duration_ms * 1000); +#endif + } } } - - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING); -#ifdef USE_I2S_LEGACY - i2s_driver_uninstall(this_speaker->parent_->get_port()); -#else - i2s_channel_disable(this_speaker->tx_handle_); - i2s_del_channel(this_speaker->tx_handle_); -#endif - - this_speaker->parent_->unlock(); } - this_speaker->delete_task_(data_buffer_size); + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STOPPING); + + if (transfer_buffer != nullptr) { + transfer_buffer.reset(); + } + + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STOPPED); + + while (true) { + // Continuously delay until the loop method deletes the task + vTaskDelay(pdMS_TO_TICKS(10)); + } } void I2SAudioSpeaker::start() { @@ -427,16 +473,7 @@ void I2SAudioSpeaker::start() { if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) return; - if (!this->task_created_ && (this->speaker_task_handle_ == nullptr)) { - xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, - &this->speaker_task_handle_); - - if (this->speaker_task_handle_ != nullptr) { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); - } else { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); - } - } + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); } void I2SAudioSpeaker::stop() { this->stop_(false); } @@ -456,61 +493,16 @@ void I2SAudioSpeaker::stop_(bool wait_on_empty) { } } -bool I2SAudioSpeaker::send_esp_err_to_event_group_(esp_err_t err) { - switch (err) { - case ESP_OK: - return false; - case ESP_ERR_INVALID_STATE: - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_STATE); - return true; - case ESP_ERR_INVALID_ARG: - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_ARG); - return true; - case ESP_ERR_INVALID_SIZE: - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE); - return true; - case ESP_ERR_NO_MEM: - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); - return true; - case ESP_ERR_NOT_SUPPORTED: - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED); - return true; - default: - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_FAIL); - return true; - } -} - -esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size) { - if (this->data_buffer_ == nullptr) { - // Allocate data buffer for temporarily storing audio from the ring buffer before writing to the I2S bus - RAMAllocator allocator; - this->data_buffer_ = allocator.allocate(data_buffer_size); - } - - if (this->data_buffer_ == nullptr) { - return ESP_ERR_NO_MEM; - } - - if (this->audio_ring_buffer_.use_count() == 0) { - // Allocate ring buffer. Uses a shared_ptr to ensure it isn't improperly deallocated. - this->audio_ring_buffer_ = RingBuffer::create(ring_buffer_size); - } - - if (this->audio_ring_buffer_ == nullptr) { - return ESP_ERR_NO_MEM; - } - - return ESP_OK; -} - esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) { + this->current_stream_info_ = audio_stream_info; // store the stream info settings the driver will use + #ifdef USE_I2S_LEGACY if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT #else if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT #endif // Can't reconfigure I2S bus, so the sample rate must match the configured value + ESP_LOGE(TAG, "Audio stream settings are not compatible with this I2S configuration"); return ESP_ERR_NOT_SUPPORTED; } @@ -521,10 +513,12 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea (i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) { #endif // Currently can't handle the case when the incoming audio has more bits per sample than the configured value + ESP_LOGE(TAG, "Audio streams with more bits per sample than the I2S speaker's configuration is not supported"); return ESP_ERR_NOT_SUPPORTED; } if (!this->parent_->try_lock()) { + ESP_LOGE(TAG, "Parent I2S bus not free"); return ESP_ERR_INVALID_STATE; } @@ -575,6 +569,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea esp_err_t err = i2s_driver_install(this->parent_->get_port(), &config, I2S_EVENT_QUEUE_COUNT, &this->i2s_event_queue_); if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to install I2S legacy driver"); // Failed to install the driver, so unlock the I2S port this->parent_->unlock(); return err; @@ -595,6 +590,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea if (err != ESP_OK) { // Failed to set the data out pin, so uninstall the driver and unlock the I2S port + ESP_LOGE(TAG, "Failed to set the data out pin"); i2s_driver_uninstall(this->parent_->get_port()); this->parent_->unlock(); } @@ -605,10 +601,12 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea .dma_desc_num = DMA_BUFFERS_COUNT, .dma_frame_num = dma_buffer_length, .auto_clear = true, + .intr_priority = 3, }; /* Allocate a new TX channel and get the handle of this channel */ esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL); if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to allocate new I2S channel"); this->parent_->unlock(); return err; } @@ -652,7 +650,11 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea // per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems to // make it play at the correct speed while sending more bits per slot. if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) { - std_slot_cfg.ws_width = static_cast(this->slot_bit_width_); + uint32_t configured_bit_width = static_cast(this->slot_bit_width_); + std_slot_cfg.ws_width = configured_bit_width; + if (configured_bit_width > 16) { + std_slot_cfg.msb_right = false; + } } #else std_slot_cfg.slot_bit_width = this->slot_bit_width_; @@ -670,54 +672,56 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg); if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize channel"); i2s_del_channel(this->tx_handle_); + this->tx_handle_ = nullptr; this->parent_->unlock(); return err; } if (this->i2s_event_queue_ == nullptr) { - this->i2s_event_queue_ = xQueueCreate(1, sizeof(bool)); + this->i2s_event_queue_ = xQueueCreate(I2S_EVENT_QUEUE_COUNT, sizeof(int64_t)); } - const i2s_event_callbacks_t callbacks = { - .on_send_q_ovf = i2s_overflow_cb, - }; - i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); - - /* Before reading data, start the TX channel first */ i2s_channel_enable(this->tx_handle_); - if (err != ESP_OK) { - i2s_del_channel(this->tx_handle_); - this->parent_->unlock(); - } #endif return err; } -void I2SAudioSpeaker::delete_task_(size_t buffer_size) { - this->audio_ring_buffer_.reset(); // Releases ownership of the shared_ptr +#ifndef USE_I2S_LEGACY +bool IRAM_ATTR I2SAudioSpeaker::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) { + int64_t now = esp_timer_get_time(); - if (this->data_buffer_ != nullptr) { - RAMAllocator allocator; - allocator.deallocate(this->data_buffer_, buffer_size); - this->data_buffer_ = nullptr; + BaseType_t need_yield1 = pdFALSE; + BaseType_t need_yield2 = pdFALSE; + BaseType_t need_yield3 = pdFALSE; + + I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx; + + if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) { + // Queue is full, so discard the oldest event and set the warning flag to inform the user + int64_t dummy; + xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1); + xEventGroupSetBitsFromISR(this_speaker->event_group_, SpeakerEventGroupBits::WARN_DROPPED_EVENT, &need_yield2); } - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPED); + xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3); - this->task_created_ = false; - vTaskDelete(nullptr); -} - -#ifndef USE_I2S_LEGACY -bool IRAM_ATTR I2SAudioSpeaker::i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) { - I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx; - bool overflow = true; - xQueueOverwrite(this_speaker->i2s_event_queue_, &overflow); - return false; + return need_yield1 | need_yield2 | need_yield3; } #endif +void I2SAudioSpeaker::stop_i2s_driver_() { +#ifdef USE_I2S_LEGACY + i2s_driver_uninstall(this->parent_->get_port()); +#else + i2s_channel_disable(this->tx_handle_); + i2s_del_channel(this->tx_handle_); + this->tx_handle_ = nullptr; +#endif + this->parent_->unlock(); +} + } // namespace i2s_audio } // namespace esphome diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index eb2a0ae756..1d03a4c495 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -72,70 +72,57 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp protected: /// @brief Function for the FreeRTOS task handling audio output. - /// After receiving the COMMAND_START signal, allocates space for the buffers, starts the I2S driver, and reads - /// audio from the ring buffer and writes audio to the I2S port. Stops immmiately after receiving the COMMAND_STOP - /// signal and stops only after the ring buffer is empty after receiving the COMMAND_STOP_GRACEFULLY signal. Stops if - /// the ring buffer hasn't read data for more than timeout_ milliseconds. When stopping, it deallocates the buffers, - /// stops the I2S driver, unlocks the I2S port, and deletes the task. It communicates the state and any errors via - /// event_group_. + /// Allocates space for the buffers, reads audio from the ring buffer and writes audio to the I2S port. Stops + /// immmiately after receiving the COMMAND_STOP signal and stops only after the ring buffer is empty after receiving + /// the COMMAND_STOP_GRACEFULLY signal. Stops if the ring buffer hasn't read data for more than timeout_ milliseconds. + /// When stopping, it deallocates the buffers. It communicates its state and any errors via ``event_group_``. /// @param params I2SAudioSpeaker component static void speaker_task(void *params); - /// @brief Sends a stop command to the speaker task via event_group_. + /// @brief Sends a stop command to the speaker task via ``event_group_``. /// @param wait_on_empty If false, sends the COMMAND_STOP signal. If true, sends the COMMAND_STOP_GRACEFULLY signal. void stop_(bool wait_on_empty); - /// @brief Sets the corresponding ERR_ESP event group bits. - /// @param err esp_err_t error code. - /// @return True if an ERR_ESP bit is set and false if err == ESP_OK - bool send_esp_err_to_event_group_(esp_err_t err); - #ifndef USE_I2S_LEGACY - static bool i2s_overflow_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx); + /// @brief Callback function used to send playback timestamps the to the speaker task. + /// @param handle (i2s_chan_handle_t) + /// @param event (i2s_event_data_t) + /// @param user_ctx (void*) User context pointer that the callback accesses + /// @return True if a higher priority task was interrupted + static bool i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx); #endif - /// @brief Allocates the data buffer and ring buffer - /// @param data_buffer_size Number of bytes to allocate for the data buffer. - /// @param ring_buffer_size Number of bytes to allocate for the ring buffer. - /// @return ESP_ERR_NO_MEM if either buffer fails to allocate - /// ESP_OK if successful - esp_err_t allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size); - /// @brief Starts the ESP32 I2S driver. /// Attempts to lock the I2S port, starts the I2S driver using the passed in stream information, and sets the data out - /// pin. If it fails, it will unlock the I2S port and uninstall the driver, if necessary. + /// pin. If it fails, it will unlock the I2S port and uninstalls the driver, if necessary. /// @param audio_stream_info Stream information for the I2S driver. /// @return ESP_ERR_NOT_ALLOWED if the I2S port can't play the incoming audio stream. /// ESP_ERR_INVALID_STATE if the I2S port is already locked. - /// ESP_ERR_INVALID_ARG if nstalling the driver or setting the data outpin fails due to a parameter error. + /// ESP_ERR_INVALID_ARG if installing the driver or setting the data outpin fails due to a parameter error. /// ESP_ERR_NO_MEM if the driver fails to install due to a memory allocation error. - /// ESP_FAIL if setting the data out pin fails due to an IO error ESP_OK if successful + /// ESP_FAIL if setting the data out pin fails due to an IO error + /// ESP_OK if successful esp_err_t start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info); - /// @brief Deletes the speaker's task. - /// Deallocates the data_buffer_ and audio_ring_buffer_, if necessary, and deletes the task. Should only be called by - /// the speaker_task itself. - /// @param buffer_size The allocated size of the data_buffer_. - void delete_task_(size_t buffer_size); + /// @brief Stops the I2S driver and unlocks the I2S port + void stop_i2s_driver_(); TaskHandle_t speaker_task_handle_{nullptr}; EventGroupHandle_t event_group_{nullptr}; QueueHandle_t i2s_event_queue_; - uint8_t *data_buffer_; - std::shared_ptr audio_ring_buffer_; + std::weak_ptr audio_ring_buffer_; uint32_t buffer_duration_ms_; optional timeout_; - bool task_created_{false}; bool pause_state_{false}; int16_t q15_volume_factor_{INT16_MAX}; - size_t bytes_written_{0}; + audio::AudioStreamInfo current_stream_info_; // The currently loaded driver's stream info #ifdef USE_I2S_LEGACY #if SOC_I2S_SUPPORTS_DAC @@ -148,8 +135,6 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp std::string i2s_comm_fmt_; i2s_chan_handle_t tx_handle_; #endif - - uint32_t accumulated_frames_written_{0}; }; } // namespace i2s_audio diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 14b7f15218..9588bccd55 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -138,9 +138,10 @@ def _validate(config): ]: raise cv.Invalid("Selected model can't run on ESP8266.") - if model == "CUSTOM": - if CONF_INIT_SEQUENCE not in config or CONF_DIMENSIONS not in config: - raise cv.Invalid("CUSTOM model requires init_sequence and dimensions") + if model == "CUSTOM" and ( + CONF_INIT_SEQUENCE not in config or CONF_DIMENSIONS not in config + ): + raise cv.Invalid("CUSTOM model requires init_sequence and dimensions") return config diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 41fd89cc58..ec0a860aa8 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -31,8 +31,6 @@ void ILI9XXXDisplay::set_madctl() { } void ILI9XXXDisplay::setup() { - ESP_LOGD(TAG, "Setting up ILI9xxx"); - this->setup_pins_(); this->init_lcd_(this->init_sequence_); this->init_lcd_(this->extra_init_sequence_.data()); diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index f6d8673a08..f880b5f736 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import hashlib import io import logging @@ -107,6 +108,24 @@ class ImageEncoder: :return: """ + @classmethod + def is_endian(cls) -> bool: + """ + Check if the image encoder supports endianness configuration + """ + return getattr(cls, "set_big_endian", None) is not None + + @classmethod + def get_options(cls) -> list[str]: + """ + Get the available options for this image encoder + """ + options = [*OPTIONS] + if not cls.is_endian(): + options.remove(CONF_BYTE_ORDER) + options.append(CONF_RAW_DATA_ID) + return options + def is_alpha_only(image: Image): """ @@ -174,9 +193,8 @@ class ImageGrayscale(ImageEncoder): b = 1 if self.invert_alpha: b ^= 0xFF - if self.transparency == CONF_ALPHA_CHANNEL: - if a != 0xFF: - b = a + if self.transparency == CONF_ALPHA_CHANNEL and a != 0xFF: + b = a self.data[self.index] = b self.index += 1 @@ -446,13 +464,14 @@ def validate_type(image_types): return validate -def validate_settings(value): +def validate_settings(value, path=()): """ Validate the settings for a single image configuration. """ conf_type = value[CONF_TYPE] type_class = IMAGE_TYPE[conf_type] - transparency = value[CONF_TRANSPARENCY].lower() + + transparency = value.get(CONF_TRANSPARENCY, CONF_OPAQUE).lower() if transparency not in type_class.allow_config: raise cv.Invalid( f"Image format '{conf_type}' cannot have transparency: {transparency}" @@ -464,11 +483,10 @@ def validate_settings(value): and CONF_INVERT_ALPHA not in type_class.allow_config ): raise cv.Invalid("No alpha channel to invert") - if value.get(CONF_BYTE_ORDER) is not None and not callable( - getattr(type_class, "set_big_endian", None) - ): + if value.get(CONF_BYTE_ORDER) is not None and not type_class.is_endian(): raise cv.Invalid( - f"Image format '{conf_type}' does not support byte order configuration" + f"Image format '{conf_type}' does not support byte order configuration", + path=path, ) if file := value.get(CONF_FILE): file = Path(file) @@ -479,7 +497,7 @@ def validate_settings(value): Image.open(file) except UnidentifiedImageError as exc: raise cv.Invalid( - f"File can't be opened as image: {file.absolute()}" + f"File can't be opened as image: {file.absolute()}", path=path ) from exc return value @@ -499,6 +517,10 @@ OPTIONS_SCHEMA = { cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean, cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True), cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(), +} + +DEFAULTS_SCHEMA = { + **OPTIONS_SCHEMA, cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE), } @@ -510,47 +532,61 @@ IMAGE_SCHEMA_NO_DEFAULTS = { **{cv.Optional(key): OPTIONS_SCHEMA[key] for key in OPTIONS}, } -BASE_SCHEMA = cv.Schema( +IMAGE_SCHEMA = cv.Schema( { **IMAGE_ID_SCHEMA, **OPTIONS_SCHEMA, - } -).add_extra(validate_settings) - -IMAGE_SCHEMA = BASE_SCHEMA.extend( - { cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE), } ) +def apply_defaults(image, defaults, path): + """ + Apply defaults to an image configuration + """ + type = image.get(CONF_TYPE, defaults.get(CONF_TYPE)) + if type is None: + raise cv.Invalid( + "Type is required either in the image config or in the defaults", path=path + ) + type_class = IMAGE_TYPE[type] + config = { + **{key: image.get(key, defaults.get(key)) for key in type_class.get_options()}, + **{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA}, + CONF_TYPE: image.get(CONF_TYPE, defaults.get(CONF_TYPE)), + } + validate_settings(config, path) + return config + + def validate_defaults(value): """ - Validate the options for images with defaults + Apply defaults to the images in the configuration and flatten to a single list. """ defaults = value[CONF_DEFAULTS] result = [] - for index, image in enumerate(value[CONF_IMAGES]): - type = image.get(CONF_TYPE, defaults.get(CONF_TYPE)) - if type is None: - raise cv.Invalid( - "Type is required either in the image config or in the defaults", - path=[CONF_IMAGES, index], - ) - type_class = IMAGE_TYPE[type] - # A default byte order should be simply ignored if the type does not support it - available_options = [*OPTIONS] - if ( - not callable(getattr(type_class, "set_big_endian", None)) - and CONF_BYTE_ORDER not in image - ): - available_options.remove(CONF_BYTE_ORDER) - config = { - **{key: image.get(key, defaults.get(key)) for key in available_options}, - **{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA}, - } - validate_settings(config) - result.append(config) + # Apply defaults to the images: list and add the list entries to the result + for index, image in enumerate(value.get(CONF_IMAGES, [])): + result.append(apply_defaults(image, defaults, [CONF_IMAGES, index])) + + # Apply defaults to images under the type keys and add them to the result + for image_type, type_config in value.items(): + type_upper = image_type.upper() + if type_upper not in IMAGE_TYPE: + continue + type_class = IMAGE_TYPE[type_upper] + if isinstance(type_config, list): + # If the type is a list, apply defaults to each entry + for index, image in enumerate(type_config): + result.append(apply_defaults(image, defaults, [image_type, index])) + else: + # Handle transparency options for the type + for trans_type in set(type_class.allow_config).intersection(type_config): + for index, image in enumerate(type_config[trans_type]): + result.append( + apply_defaults(image, defaults, [image_type, trans_type, index]) + ) return result @@ -562,16 +598,20 @@ def typed_image_schema(image_type): cv.Schema( { cv.Optional(t.lower()): cv.ensure_list( - BASE_SCHEMA.extend( - { - cv.Optional( - CONF_TRANSPARENCY, default=t - ): validate_transparency((t,)), - cv.Optional(CONF_TYPE, default=image_type): validate_type( - (image_type,) - ), - } - ) + { + **IMAGE_ID_SCHEMA, + **{ + cv.Optional(key): OPTIONS_SCHEMA[key] + for key in OPTIONS + if key != CONF_TRANSPARENCY + }, + cv.Optional( + CONF_TRANSPARENCY, default=t + ): validate_transparency((t,)), + cv.Optional(CONF_TYPE, default=image_type): validate_type( + (image_type,) + ), + } ) for t in IMAGE_TYPE[image_type].allow_config.intersection( TRANSPARENCY_TYPES @@ -580,46 +620,44 @@ def typed_image_schema(image_type): ), # Allow a default configuration with no transparency preselected cv.ensure_list( - BASE_SCHEMA.extend( - { - cv.Optional( - CONF_TRANSPARENCY, default=CONF_OPAQUE - ): validate_transparency(), - cv.Optional(CONF_TYPE, default=image_type): validate_type( - (image_type,) - ), - } - ) + { + **IMAGE_SCHEMA_NO_DEFAULTS, + cv.Optional(CONF_TYPE, default=image_type): validate_type( + (image_type,) + ), + } ), ) # The config schema can be a (possibly empty) single list of images, -# or a dictionary of image types each with a list of images -# or a dictionary with keys `defaults:` and `images:` +# or a dictionary with optional keys `defaults:`, `images:` and the image types -def _config_schema(config): - if isinstance(config, list): - return cv.Schema([IMAGE_SCHEMA])(config) - if not isinstance(config, dict): +def _config_schema(value): + if isinstance(value, list) or ( + isinstance(value, dict) and (CONF_ID in value or CONF_FILE in value) + ): + return cv.ensure_list(cv.All(IMAGE_SCHEMA, validate_settings))(value) + if not isinstance(value, dict): raise cv.Invalid( - "Badly formed image configuration, expected a list or a dictionary" + "Badly formed image configuration, expected a list or a dictionary", ) - if CONF_DEFAULTS in config or CONF_IMAGES in config: - return validate_defaults( - cv.Schema( - { - cv.Required(CONF_DEFAULTS): OPTIONS_SCHEMA, - cv.Required(CONF_IMAGES): cv.ensure_list(IMAGE_SCHEMA_NO_DEFAULTS), - } - )(config) - ) - if CONF_ID in config or CONF_FILE in config: - return cv.ensure_list(IMAGE_SCHEMA)([config]) - return cv.Schema( - {cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE} - )(config) + return cv.All( + cv.Schema( + { + cv.Optional(CONF_DEFAULTS, default={}): DEFAULTS_SCHEMA, + cv.Optional(CONF_IMAGES, default=[]): cv.ensure_list( + { + **IMAGE_SCHEMA_NO_DEFAULTS, + cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE), + } + ), + **{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}, + } + ), + validate_defaults, + )(value) CONFIG_SCHEMA = _config_schema @@ -668,14 +706,12 @@ async def write_image(config, all_frames=False): else Image.Dither.FLOYDSTEINBERG ) type = config[CONF_TYPE] - transparency = config[CONF_TRANSPARENCY] + transparency = config.get(CONF_TRANSPARENCY, CONF_OPAQUE) invert_alpha = config[CONF_INVERT_ALPHA] frame_count = 1 if all_frames: - try: + with contextlib.suppress(AttributeError): frame_count = image.n_frames - except AttributeError: - pass if frame_count <= 1: _LOGGER.warning("Image file %s has no animation frames", path) @@ -701,14 +737,9 @@ async def write_image(config, all_frames=False): async def to_code(config): - if isinstance(config, list): - for entry in config: - await to_code(entry) - elif CONF_ID not in config: - for entry in config.values(): - await to_code(entry) - else: - prog_arr, width, height, image_type, trans_value, _ = await write_image(config) + # By now the config should be a simple list. + for entry in config: + prog_arr, width, height, image_type, trans_value, _ = await write_image(entry) cg.new_Pvariable( - config[CONF_ID], prog_arr, width, height, image_type, trans_value + entry[CONF_ID], prog_arr, width, height, image_type, trans_value ) diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 544af212e0..568b200a85 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -27,14 +27,13 @@ def validate_logger(config): logger_conf = fv.full_config.get()[CONF_LOGGER] if logger_conf[CONF_BAUD_RATE] == 0: raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") - if CORE.using_esp_idf: - if ( - logger_conf[CONF_HARDWARE_UART] == USB_CDC - and get_esp32_variant() == VARIANT_ESP32S3 - ): - raise cv.Invalid( - "improv_serial does not support the selected logger hardware_uart" - ) + if CORE.using_esp_idf and ( + logger_conf[CONF_HARDWARE_UART] == USB_CDC + and get_esp32_variant() == VARIANT_ESP32S3 + ): + raise cv.Invalid( + "improv_serial does not support the selected logger hardware_uart" + ) return config diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index 52a3b1e067..ea8c5cea9d 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -34,7 +34,6 @@ static const uint8_t INA219_REGISTER_CURRENT = 0x04; static const uint8_t INA219_REGISTER_CALIBRATION = 0x05; void INA219Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // Config Register // 0bx000000000000000 << 15 RESET Bit (1 -> trigger reset) if (!this->write_byte_16(INA219_REGISTER_CONFIG, 0x8000)) { diff --git a/esphome/components/ina226/ina226.cpp b/esphome/components/ina226/ina226.cpp index 52e7127708..c4d4fb896e 100644 --- a/esphome/components/ina226/ina226.cpp +++ b/esphome/components/ina226/ina226.cpp @@ -37,8 +37,6 @@ static const uint16_t INA226_ADC_TIMES[] = {140, 204, 332, 588, 1100, 2116, 4156 static const uint16_t INA226_ADC_AVG_SAMPLES[] = {1, 4, 16, 64, 128, 256, 512, 1024}; void INA226Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - ConfigurationRegister config; config.reset = 1; diff --git a/esphome/components/ina260/ina260.cpp b/esphome/components/ina260/ina260.cpp index 2b6208f60f..9dd922cec2 100644 --- a/esphome/components/ina260/ina260.cpp +++ b/esphome/components/ina260/ina260.cpp @@ -35,8 +35,6 @@ static const uint8_t INA260_REGISTER_MANUFACTURE_ID = 0xFE; static const uint8_t INA260_REGISTER_DEVICE_ID = 0xFF; void INA260Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Reset device on setup if (!this->write_byte_16(INA260_REGISTER_CONFIG, 0x8000)) { this->error_code_ = DEVICE_RESET_FAILED; diff --git a/esphome/components/ina2xx_base/__init__.py b/esphome/components/ina2xx_base/__init__.py index 7c3d3c8899..ff70f217ec 100644 --- a/esphome/components/ina2xx_base/__init__.py +++ b/esphome/components/ina2xx_base/__init__.py @@ -78,11 +78,8 @@ def validate_model_config(config): model = config[CONF_MODEL] for key in config: - if key in SENSOR_MODEL_OPTIONS: - if model not in SENSOR_MODEL_OPTIONS[key]: - raise cv.Invalid( - f"Device model '{model}' does not support '{key}' sensor" - ) + if key in SENSOR_MODEL_OPTIONS and model not in SENSOR_MODEL_OPTIONS[key]: + raise cv.Invalid(f"Device model '{model}' does not support '{key}' sensor") tempco = config[CONF_TEMPERATURE_COEFFICIENT] if tempco > 0 and model not in ["INA228", "INA229"]: diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 2112a28b02..35a94e3989 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -50,8 +50,6 @@ static bool check_model_and_device_match(INAModel model, uint16_t dev_id) { } void INA2XX::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - if (!this->reset_config_()) { ESP_LOGE(TAG, "Reset failed, check connection"); this->mark_failed(); diff --git a/esphome/components/ina3221/ina3221.cpp b/esphome/components/ina3221/ina3221.cpp index 35e79462ab..8243764147 100644 --- a/esphome/components/ina3221/ina3221.cpp +++ b/esphome/components/ina3221/ina3221.cpp @@ -22,7 +22,6 @@ static const uint8_t INA3221_REGISTER_CHANNEL3_BUS_VOLTAGE = 0x06; // A0 = SCL -> 0x43 void INA3221Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // Config Register // 0bx000000000000000 << 15 RESET Bit (1 -> trigger reset) if (!this->write_byte_16(INA3221_REGISTER_CONFIG, 0x8000)) { diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp index 85844647f2..28ac55d6de 100644 --- a/esphome/components/internal_temperature/internal_temperature.cpp +++ b/esphome/components/internal_temperature/internal_temperature.cpp @@ -84,8 +84,6 @@ void InternalTemperatureSensor::setup() { #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ defined(USE_ESP32_VARIANT_ESP32P4) - ESP_LOGCONFIG(TAG, "Running setup"); - temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); esp_err_t result = temperature_sensor_install(&tsens_config, &tsensNew); diff --git a/esphome/components/interval/interval.h b/esphome/components/interval/interval.h index 8f904b104d..e419841e6c 100644 --- a/esphome/components/interval/interval.h +++ b/esphome/components/interval/interval.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/log.h" #include "esphome/core/automation.h" namespace esphome { @@ -8,16 +9,12 @@ namespace interval { class IntervalTrigger : public Trigger<>, public PollingComponent { public: - void update() override { - if (this->started_) - this->trigger(); - } + void update() override { this->trigger(); } void setup() override { - if (this->startup_delay_ == 0) { - this->started_ = true; - } else { - this->set_timeout(this->startup_delay_, [this] { this->started_ = true; }); + if (this->startup_delay_ != 0) { + this->stop_poller(); + this->set_timeout(this->startup_delay_, [this] { this->start_poller(); }); } } @@ -25,7 +22,6 @@ class IntervalTrigger : public Trigger<>, public PollingComponent { protected: uint32_t startup_delay_{0}; - bool started_{false}; }; } // namespace interval diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index 9773bf67ce..87aa823c0d 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import coroutine_with_priority -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] json_ns = cg.esphome_ns.namespace("json") CONFIG_SCHEMA = cv.All( diff --git a/esphome/components/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp index 714df0b538..3aedac3f5f 100644 --- a/esphome/components/kmeteriso/kmeteriso.cpp +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -14,7 +14,6 @@ static const uint8_t KMETER_INTERNAL_TEMP_VAL_REG = 0x10; static const uint8_t KMETER_FIRMWARE_VERSION_REG = 0xFE; void KMeterISOComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->error_code_ = NONE; // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries @@ -46,7 +45,6 @@ void KMeterISOComponent::setup() { this->mark_failed(); return; } - ESP_LOGCONFIG(TAG, "The device was successfully setup."); } float KMeterISOComponent::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/lc709203f/lc709203f.cpp b/esphome/components/lc709203f/lc709203f.cpp index d95a2c1d5e..e5d12a75d4 100644 --- a/esphome/components/lc709203f/lc709203f.cpp +++ b/esphome/components/lc709203f/lc709203f.cpp @@ -49,8 +49,6 @@ void Lc709203f::setup() { // initialization code checks the return code from those functions. If they don't return // NO_ERROR (0x00), that part of the initialization aborts and will be retried on the next // call to update(). - ESP_LOGCONFIG(TAG, "Running setup"); - // Set power mode to on. Note that, unlike some other similar devices, in sleep mode the IC // does not record power usage. If there is significant power consumption during sleep mode, // the pack RSOC will likely no longer be correct. Because of that, I do not implement diff --git a/esphome/components/lcd_gpio/display.py b/esphome/components/lcd_gpio/display.py index caa73194c9..0a77daf336 100644 --- a/esphome/components/lcd_gpio/display.py +++ b/esphome/components/lcd_gpio/display.py @@ -41,9 +41,7 @@ CONFIG_SCHEMA = lcd_base.LCD_SCHEMA.extend( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await lcd_base.setup_lcd_display(var, config) - pins_ = [] - for conf in config[CONF_DATA_PINS]: - pins_.append(await cg.gpio_pin_expression(conf)) + pins_ = [await cg.gpio_pin_expression(conf) for conf in config[CONF_DATA_PINS]] cg.add(var.set_data_pins(*pins_)) enable = await cg.gpio_pin_expression(config[CONF_ENABLE_PIN]) cg.add(var.set_enable_pin(enable)) diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.cpp b/esphome/components/lcd_gpio/gpio_lcd_display.cpp index afa74643fb..ae6e1194b8 100644 --- a/esphome/components/lcd_gpio/gpio_lcd_display.cpp +++ b/esphome/components/lcd_gpio/gpio_lcd_display.cpp @@ -7,7 +7,6 @@ namespace lcd_gpio { static const char *const TAG = "lcd_gpio"; void GPIOLCDDisplay::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->rs_pin_->setup(); // OUTPUT this->rs_pin_->digital_write(false); if (this->rw_pin_ != nullptr) { diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.cpp b/esphome/components/lcd_pcf8574/pcf8574_display.cpp index 0f06548b13..d582eead91 100644 --- a/esphome/components/lcd_pcf8574/pcf8574_display.cpp +++ b/esphome/components/lcd_pcf8574/pcf8574_display.cpp @@ -11,7 +11,6 @@ static const uint8_t LCD_DISPLAY_BACKLIGHT_ON = 0x08; static const uint8_t LCD_DISPLAY_BACKLIGHT_OFF = 0x00; void PCF8574LCDDisplay::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->backlight_value_ = LCD_DISPLAY_BACKLIGHT_ON; if (!this->write_bytes(this->backlight_value_, nullptr, 0)) { this->mark_failed(); diff --git a/esphome/components/ld2410/__init__.py b/esphome/components/ld2410/__init__.py index c58b9a4017..b492bbcd14 100644 --- a/esphome/components/ld2410/__init__.py +++ b/esphome/components/ld2410/__init__.py @@ -5,6 +5,7 @@ from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PASSWORD, CONF_THROTTLE, CONF_TIMEOUT +AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] CODEOWNERS = ["@sebcaps", "@regevbr"] MULTI_CONF = True @@ -13,18 +14,16 @@ ld2410_ns = cg.esphome_ns.namespace("ld2410") LD2410Component = ld2410_ns.class_("LD2410Component", cg.Component, uart.UARTDevice) CONF_LD2410_ID = "ld2410_id" - CONF_MAX_MOVE_DISTANCE = "max_move_distance" CONF_MAX_STILL_DISTANCE = "max_still_distance" -CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)] CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)] +CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)] CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(LD2410Component), - cv.Optional(CONF_THROTTLE, default="1000ms"): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(min=cv.TimePeriod(milliseconds=1)), + cv.Optional(CONF_THROTTLE): cv.invalid( + f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" ), cv.Optional(CONF_MAX_MOVE_DISTANCE): cv.invalid( f"The '{CONF_MAX_MOVE_DISTANCE}' option has been moved to the '{CONF_MAX_MOVE_DISTANCE}'" @@ -74,7 +73,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) - cg.add(var.set_throttle(config[CONF_THROTTLE])) CALIBRATION_ACTION_SCHEMA = maybe_simple_id( diff --git a/esphome/components/ld2410/binary_sensor.py b/esphome/components/ld2410/binary_sensor.py index d2938754e9..4e35f67fbe 100644 --- a/esphome/components/ld2410/binary_sensor.py +++ b/esphome/components/ld2410/binary_sensor.py @@ -22,19 +22,23 @@ CONFIG_SCHEMA = { cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_ACCOUNT, ), cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_MOTION, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_MOTION_SENSOR, ), cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_MOTION_SENSOR, ), cv.Optional(CONF_OUT_PIN_PRESENCE_STATUS): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_PRESENCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_ACCOUNT, ), } diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 8f1fb0ee9d..5c3af54ad8 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -1,6 +1,5 @@ #include "ld2410.h" -#include #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif @@ -10,10 +9,6 @@ #include "esphome/core/application.h" -#define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) -#define highbyte(val) (uint8_t)((val) >> 8) -#define lowbyte(val) (uint8_t)((val) &0xff) - namespace esphome { namespace ld2410 { @@ -165,6 +160,9 @@ static constexpr uint8_t CMD_BLUETOOTH = 0xA4; static constexpr uint8_t CMD_MAX_MOVE_VALUE = 0x00; static constexpr uint8_t CMD_MAX_STILL_VALUE = 0x01; static constexpr uint8_t CMD_DURATION_VALUE = 0x02; +// Bitmasks for target states +static constexpr uint8_t MOVE_BITMASK = 0x01; +static constexpr uint8_t STILL_BITMASK = 0x02; // Header & Footer size static constexpr uint8_t HEADER_FOOTER_SIZE = 4; // Command Header & Footer @@ -190,9 +188,8 @@ void LD2410Component::dump_config() { ESP_LOGCONFIG(TAG, "LD2410:\n" " Firmware version: %s\n" - " MAC address: %s\n" - " Throttle: %u ms", - version.c_str(), mac_str.c_str(), this->throttle_); + " MAC address: %s", + version.c_str(), mac_str.c_str()); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); @@ -202,17 +199,17 @@ void LD2410Component::dump_config() { #endif #ifdef USE_SENSOR ESP_LOGCONFIG(TAG, "Sensors:"); - LOG_SENSOR(" ", "Light", this->light_sensor_); - LOG_SENSOR(" ", "DetectionDistance", this->detection_distance_sensor_); - LOG_SENSOR(" ", "MovingTargetDistance", this->moving_target_distance_sensor_); - LOG_SENSOR(" ", "MovingTargetEnergy", this->moving_target_energy_sensor_); - LOG_SENSOR(" ", "StillTargetDistance", this->still_target_distance_sensor_); - LOG_SENSOR(" ", "StillTargetEnergy", this->still_target_energy_sensor_); - for (sensor::Sensor *s : this->gate_move_sensors_) { - LOG_SENSOR(" ", "GateMove", s); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "Light", this->light_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "DetectionDistance", this->detection_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetDistance", this->moving_target_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetEnergy", this->moving_target_energy_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetDistance", this->still_target_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetEnergy", this->still_target_energy_sensor_); + for (auto &s : this->gate_move_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateMove", s); } - for (sensor::Sensor *s : this->gate_still_sensors_) { - LOG_SENSOR(" ", "GateStill", s); + for (auto &s : this->gate_still_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateStill", s); } #endif #ifdef USE_TEXT_SENSOR @@ -253,10 +250,7 @@ void LD2410Component::dump_config() { #endif } -void LD2410Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->read_all_info(); -} +void LD2410Component::setup() { this->read_all_info(); } void LD2410Component::read_all_info() { this->set_config_mode_(true); @@ -304,16 +298,13 @@ void LD2410Component::send_command_(uint8_t command, const uint8_t *command_valu } // frame footer bytes this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)); - // FIXME to remove - delay(50); // NOLINT + + if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) { + delay(50); // NOLINT + } } void LD2410Component::handle_periodic_data_() { - // Reduce data update rate to reduce home assistant database growth - // Check this first to prevent unnecessary processing done in later checks/parsing - if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { - return; - } // 4 frame header bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame footer bytes // data header=0xAA, data footer=0x55, crc=0x00 if (this->buffer_pos_ < 12 || !ld2410::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) || @@ -321,9 +312,6 @@ void LD2410Component::handle_periodic_data_() { this->buffer_data_[this->buffer_pos_ - 5] != CHECK) { return; } - // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately - this->last_periodic_millis_ = App.get_loop_component_start_time(); - /* Data Type: 7th 0x01: Engineering mode @@ -348,10 +336,10 @@ void LD2410Component::handle_periodic_data_() { this->target_binary_sensor_->publish_state(target_state != 0x00); } if (this->moving_target_binary_sensor_ != nullptr) { - this->moving_target_binary_sensor_->publish_state(CHECK_BIT(target_state, 0)); + this->moving_target_binary_sensor_->publish_state(target_state & MOVE_BITMASK); } if (this->still_target_binary_sensor_ != nullptr) { - this->still_target_binary_sensor_->publish_state(CHECK_BIT(target_state, 1)); + this->still_target_binary_sensor_->publish_state(target_state & STILL_BITMASK); } #endif /* @@ -362,89 +350,51 @@ void LD2410Component::handle_periodic_data_() { Detect distance: 16~17th bytes */ #ifdef USE_SENSOR - if (this->moving_target_distance_sensor_ != nullptr) { - int new_moving_target_distance = - ld2410::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]); - if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance) - this->moving_target_distance_sensor_->publish_state(new_moving_target_distance); - } - if (this->moving_target_energy_sensor_ != nullptr) { - int new_moving_target_energy = this->buffer_data_[MOVING_ENERGY]; - if (this->moving_target_energy_sensor_->get_state() != new_moving_target_energy) - this->moving_target_energy_sensor_->publish_state(new_moving_target_energy); - } - if (this->still_target_distance_sensor_ != nullptr) { - int new_still_target_distance = - ld2410::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]); - if (this->still_target_distance_sensor_->get_state() != new_still_target_distance) - this->still_target_distance_sensor_->publish_state(new_still_target_distance); - } - if (this->still_target_energy_sensor_ != nullptr) { - int new_still_target_energy = this->buffer_data_[STILL_ENERGY]; - if (this->still_target_energy_sensor_->get_state() != new_still_target_energy) - this->still_target_energy_sensor_->publish_state(new_still_target_energy); - } - if (this->detection_distance_sensor_ != nullptr) { - int new_detect_distance = - ld2410::two_byte_to_int(this->buffer_data_[DETECT_DISTANCE_LOW], this->buffer_data_[DETECT_DISTANCE_HIGH]); - if (this->detection_distance_sensor_->get_state() != new_detect_distance) - this->detection_distance_sensor_->publish_state(new_detect_distance); - } + SAFE_PUBLISH_SENSOR( + this->moving_target_distance_sensor_, + ld2410::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]) + SAFE_PUBLISH_SENSOR( + this->still_target_distance_sensor_, + ld2410::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH])); + SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]); + SAFE_PUBLISH_SENSOR( + this->detection_distance_sensor_, + ld2410::two_byte_to_int(this->buffer_data_[DETECT_DISTANCE_LOW], this->buffer_data_[DETECT_DISTANCE_HIGH])); + if (engineering_mode) { /* Moving distance range: 18th byte Still distance range: 19th byte 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]; - if (s != nullptr) { - s->publish_state(this->buffer_data_[MOVING_SENSOR_START + i]); - } + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) } /* Still energy: 29~37th bytes */ - for (std::vector::size_type i = 0; i != this->gate_still_sensors_.size(); i++) { - sensor::Sensor *s = this->gate_still_sensors_[i]; - if (s != nullptr) { - s->publish_state(this->buffer_data_[STILL_SENSOR_START + i]); - } + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) } /* Light sensor: 38th bytes */ - if (this->light_sensor_ != nullptr) { - int new_light_sensor = this->buffer_data_[LIGHT_SENSOR]; - if (this->light_sensor_->get_state() != new_light_sensor) { - this->light_sensor_->publish_state(new_light_sensor); - } - } + SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) } else { - for (auto *s : this->gate_move_sensors_) { - if (s != nullptr && !std::isnan(s->get_state())) { - s->publish_state(NAN); - } + for (auto &gate_move_sensor : this->gate_move_sensors_) { + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor) } - for (auto *s : this->gate_still_sensors_) { - if (s != nullptr && !std::isnan(s->get_state())) { - s->publish_state(NAN); - } - } - if (this->light_sensor_ != nullptr && !std::isnan(this->light_sensor_->get_state())) { - this->light_sensor_->publish_state(NAN); + for (auto &gate_still_sensor : this->gate_still_sensors_) { + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor) } + SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_) } #endif #ifdef USE_BINARY_SENSOR - if (engineering_mode) { - if (this->out_pin_presence_status_binary_sensor_ != nullptr) { - this->out_pin_presence_status_binary_sensor_->publish_state(this->buffer_data_[OUT_PIN_SENSOR] == 0x01); - } - } else { - if (this->out_pin_presence_status_binary_sensor_ != nullptr) { - this->out_pin_presence_status_binary_sensor_->publish_state(false); - } + if (this->out_pin_presence_status_binary_sensor_ != nullptr) { + this->out_pin_presence_status_binary_sensor_->publish_state( + engineering_mode ? this->buffer_data_[OUT_PIN_SENSOR] == 0x01 : false); } #endif } @@ -824,8 +774,14 @@ void LD2410Component::set_light_out_control() { } #ifdef USE_SENSOR -void LD2410Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { this->gate_move_sensors_[gate] = s; } -void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { this->gate_still_sensors_[gate] = s; } +// These could leak memory, but they are only set once prior to 'setup()' and should never be used again. +void LD2410Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { + this->gate_move_sensors_[gate] = new SensorWithDedup(s); +} + +void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { + this->gate_still_sensors_[gate] = new SensorWithDedup(s); +} #endif } // namespace ld2410 diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 8bd1dbcb5a..54fe1ce14d 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -22,15 +22,20 @@ #ifdef USE_TEXT_SENSOR #include "esphome/components/text_sensor/text_sensor.h" #endif +#include "esphome/components/ld24xx/ld24xx.h" #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" +#include + namespace esphome { namespace ld2410 { -static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer -static const uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 +using namespace ld24xx; + +static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410 class LD2410Component : public Component, public uart::UARTDevice { #ifdef USE_BINARY_SENSOR @@ -40,12 +45,12 @@ class LD2410Component : public Component, public uart::UARTDevice { SUB_BINARY_SENSOR(target) #endif #ifdef USE_SENSOR - SUB_SENSOR(light) - SUB_SENSOR(detection_distance) - SUB_SENSOR(moving_target_distance) - SUB_SENSOR(moving_target_energy) - SUB_SENSOR(still_target_distance) - SUB_SENSOR(still_target_energy) + SUB_SENSOR_WITH_DEDUP(light, uint8_t) + SUB_SENSOR_WITH_DEDUP(detection_distance, int) + SUB_SENSOR_WITH_DEDUP(moving_target_distance, int) + SUB_SENSOR_WITH_DEDUP(moving_target_energy, uint8_t) + SUB_SENSOR_WITH_DEDUP(still_target_distance, int) + SUB_SENSOR_WITH_DEDUP(still_target_energy, uint8_t) #endif #ifdef USE_TEXT_SENSOR SUB_TEXT_SENSOR(version) @@ -88,7 +93,6 @@ class LD2410Component : public Component, public uart::UARTDevice { void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s); void set_gate_still_sensor(uint8_t gate, sensor::Sensor *s); #endif - void set_throttle(uint16_t value) { this->throttle_ = value; }; void set_bluetooth_password(const std::string &password); void set_engineering_mode(bool enable); void read_all_info(); @@ -111,8 +115,6 @@ class LD2410Component : public Component, public uart::UARTDevice { void query_light_control_(); void restart_(); - uint32_t last_periodic_millis_ = 0; - uint16_t throttle_ = 0; uint8_t light_function_ = 0; uint8_t light_threshold_ = 0; uint8_t out_pin_level_ = 0; @@ -122,12 +124,12 @@ class LD2410Component : public Component, public uart::UARTDevice { uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; bool bluetooth_on_{false}; #ifdef USE_NUMBER - std::vector gate_move_threshold_numbers_ = std::vector(TOTAL_GATES); - std::vector gate_still_threshold_numbers_ = std::vector(TOTAL_GATES); + std::array gate_move_threshold_numbers_{}; + std::array gate_still_threshold_numbers_{}; #endif #ifdef USE_SENSOR - std::vector gate_move_sensors_ = std::vector(TOTAL_GATES); - std::vector gate_still_sensors_ = std::vector(TOTAL_GATES); + std::array *, TOTAL_GATES> gate_move_sensors_{}; + std::array *, TOTAL_GATES> gate_still_sensors_{}; #endif }; diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index 92245ea9a6..fca2b2ceca 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -18,42 +18,50 @@ from esphome.const import ( from . import CONF_LD2410_ID, LD2410Component DEPENDENCIES = ["ld2410"] -CONF_STILL_DISTANCE = "still_distance" -CONF_MOVING_ENERGY = "moving_energy" -CONF_STILL_ENERGY = "still_energy" + CONF_DETECTION_DISTANCE = "detection_distance" CONF_MOVE_ENERGY = "move_energy" +CONF_MOVING_ENERGY = "moving_energy" +CONF_STILL_DISTANCE = "still_distance" +CONF_STILL_ENERGY = "still_energy" + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_CENTIMETER, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_CENTIMETER, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], icon=ICON_FLASH, + unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], icon=ICON_LIGHTBULB, ), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_CENTIMETER, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, ), } ) @@ -63,14 +71,20 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(f"g{x}"): cv.Schema( { cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + ], icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + ], icon=ICON_FLASH, + unit_of_measurement=UNIT_PERCENT, ), } ) diff --git a/esphome/components/ld2412/__init__.py b/esphome/components/ld2412/__init__.py new file mode 100644 index 0000000000..e701d0bda9 --- /dev/null +++ b/esphome/components/ld2412/__init__.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_THROTTLE + +AUTO_LOAD = ["ld24xx"] +CODEOWNERS = ["@Rihan9"] +DEPENDENCIES = ["uart"] +MULTI_CONF = True + +LD2412_ns = cg.esphome_ns.namespace("ld2412") +LD2412Component = LD2412_ns.class_("LD2412Component", cg.Component, uart.UARTDevice) + +CONF_LD2412_ID = "ld2412_id" + +CONF_MAX_MOVE_DISTANCE = "max_move_distance" +CONF_MAX_STILL_DISTANCE = "max_still_distance" +CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)] +CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)] + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LD2412Component), + cv.Optional(CONF_THROTTLE): cv.invalid( + f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "ld2412", + require_tx=True, + require_rx=True, + parity="NONE", + stop_bits=1, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/ld2412/binary_sensor.py b/esphome/components/ld2412/binary_sensor.py new file mode 100644 index 0000000000..aa1b0d2cd8 --- /dev/null +++ b/esphome/components/ld2412/binary_sensor.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HAS_MOVING_TARGET, + CONF_HAS_STILL_TARGET, + CONF_HAS_TARGET, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_RUNNING, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_ACCOUNT, + ICON_MOTION_SENSOR, +) + +from . import CONF_LD2412_ID, LD2412Component + +DEPENDENCIES = ["ld2412"] + +CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS = "dynamic_background_correction_status" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional( + CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS + ): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_RUNNING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_ACCOUNT, + ), + cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_ACCOUNT, + ), + cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_MOTION, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_MOTION_SENSOR, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if dynamic_background_correction_status_config := config.get( + CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS + ): + sens = await binary_sensor.new_binary_sensor( + dynamic_background_correction_status_config + ) + cg.add( + LD2412_component.set_dynamic_background_correction_status_binary_sensor( + sens + ) + ) + if has_target_config := config.get(CONF_HAS_TARGET): + sens = await binary_sensor.new_binary_sensor(has_target_config) + cg.add(LD2412_component.set_target_binary_sensor(sens)) + if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET): + sens = await binary_sensor.new_binary_sensor(has_moving_target_config) + cg.add(LD2412_component.set_moving_target_binary_sensor(sens)) + if has_still_target_config := config.get(CONF_HAS_STILL_TARGET): + sens = await binary_sensor.new_binary_sensor(has_still_target_config) + cg.add(LD2412_component.set_still_target_binary_sensor(sens)) diff --git a/esphome/components/ld2412/button/__init__.py b/esphome/components/ld2412/button/__init__.py new file mode 100644 index 0000000000..e78cad4b88 --- /dev/null +++ b/esphome/components/ld2412/button/__init__.py @@ -0,0 +1,74 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ( + CONF_FACTORY_RESET, + CONF_RESTART, + DEVICE_CLASS_RESTART, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_DATABASE, + ICON_PULSE, + ICON_RESTART, + ICON_RESTART_ALERT, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +FactoryResetButton = LD2412_ns.class_("FactoryResetButton", button.Button) +QueryButton = LD2412_ns.class_("QueryButton", button.Button) +RestartButton = LD2412_ns.class_("RestartButton", button.Button) +StartDynamicBackgroundCorrectionButton = LD2412_ns.class_( + "StartDynamicBackgroundCorrectionButton", button.Button +) + +CONF_QUERY_PARAMS = "query_params" +CONF_START_DYNAMIC_BACKGROUND_CORRECTION = "start_dynamic_background_correction" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_FACTORY_RESET): button.button_schema( + FactoryResetButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART_ALERT, + ), + cv.Optional(CONF_QUERY_PARAMS): button.button_schema( + QueryButton, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_DATABASE, + ), + cv.Optional(CONF_RESTART): button.button_schema( + RestartButton, + device_class=DEVICE_CLASS_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + icon=ICON_RESTART, + ), + cv.Optional(CONF_START_DYNAMIC_BACKGROUND_CORRECTION): button.button_schema( + StartDynamicBackgroundCorrectionButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_PULSE, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if factory_reset_config := config.get(CONF_FACTORY_RESET): + b = await button.new_button(factory_reset_config) + await cg.register_parented(b, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_factory_reset_button(b)) + if query_params_config := config.get(CONF_QUERY_PARAMS): + b = await button.new_button(query_params_config) + await cg.register_parented(b, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_query_button(b)) + if restart_config := config.get(CONF_RESTART): + b = await button.new_button(restart_config) + await cg.register_parented(b, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_restart_button(b)) + if start_dynamic_background_correction_config := config.get( + CONF_START_DYNAMIC_BACKGROUND_CORRECTION + ): + b = await button.new_button(start_dynamic_background_correction_config) + await cg.register_parented(b, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_start_dynamic_background_correction_button(b)) diff --git a/esphome/components/ld2412/button/factory_reset_button.cpp b/esphome/components/ld2412/button/factory_reset_button.cpp new file mode 100644 index 0000000000..7ee85bc8f9 --- /dev/null +++ b/esphome/components/ld2412/button/factory_reset_button.cpp @@ -0,0 +1,9 @@ +#include "factory_reset_button.h" + +namespace esphome { +namespace ld2412 { + +void FactoryResetButton::press_action() { this->parent_->factory_reset(); } + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/factory_reset_button.h b/esphome/components/ld2412/button/factory_reset_button.h new file mode 100644 index 0000000000..36a3fffcd5 --- /dev/null +++ b/esphome/components/ld2412/button/factory_reset_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class FactoryResetButton : public button::Button, public Parented { + public: + FactoryResetButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/query_button.cpp b/esphome/components/ld2412/button/query_button.cpp new file mode 100644 index 0000000000..536f74427f --- /dev/null +++ b/esphome/components/ld2412/button/query_button.cpp @@ -0,0 +1,9 @@ +#include "query_button.h" + +namespace esphome { +namespace ld2412 { + +void QueryButton::press_action() { this->parent_->read_all_info(); } + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/query_button.h b/esphome/components/ld2412/button/query_button.h new file mode 100644 index 0000000000..595ef6d1e9 --- /dev/null +++ b/esphome/components/ld2412/button/query_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class QueryButton : public button::Button, public Parented { + public: + QueryButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/restart_button.cpp b/esphome/components/ld2412/button/restart_button.cpp new file mode 100644 index 0000000000..aca0d17841 --- /dev/null +++ b/esphome/components/ld2412/button/restart_button.cpp @@ -0,0 +1,9 @@ +#include "restart_button.h" + +namespace esphome { +namespace ld2412 { + +void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/restart_button.h b/esphome/components/ld2412/button/restart_button.h new file mode 100644 index 0000000000..5cd582e2a3 --- /dev/null +++ b/esphome/components/ld2412/button/restart_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class RestartButton : public button::Button, public Parented { + public: + RestartButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp new file mode 100644 index 0000000000..9b37243b82 --- /dev/null +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp @@ -0,0 +1,11 @@ +#include "start_dynamic_background_correction_button.h" + +#include "restart_button.h" + +namespace esphome { +namespace ld2412 { + +void StartDynamicBackgroundCorrectionButton::press_action() { this->parent_->start_dynamic_background_correction(); } + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.h b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h new file mode 100644 index 0000000000..3af0a8a149 --- /dev/null +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class StartDynamicBackgroundCorrectionButton : public button::Button, public Parented { + public: + StartDynamicBackgroundCorrectionButton() = default; + + protected: + void press_action() override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp new file mode 100644 index 0000000000..63af69ce0d --- /dev/null +++ b/esphome/components/ld2412/ld2412.cpp @@ -0,0 +1,861 @@ +#include "ld2412.h" + +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ld2412 { + +static const char *const TAG = "ld2412"; +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; + +enum BaudRate : uint8_t { + BAUD_RATE_9600 = 1, + BAUD_RATE_19200 = 2, + BAUD_RATE_38400 = 3, + BAUD_RATE_57600 = 4, + BAUD_RATE_115200 = 5, + BAUD_RATE_230400 = 6, + BAUD_RATE_256000 = 7, + BAUD_RATE_460800 = 8, +}; + +enum DistanceResolution : uint8_t { + DISTANCE_RESOLUTION_0_2 = 0x03, + DISTANCE_RESOLUTION_0_5 = 0x01, + DISTANCE_RESOLUTION_0_75 = 0x00, +}; + +enum LightFunction : uint8_t { + LIGHT_FUNCTION_OFF = 0x00, + LIGHT_FUNCTION_BELOW = 0x01, + LIGHT_FUNCTION_ABOVE = 0x02, +}; + +enum OutPinLevel : uint8_t { + OUT_PIN_LEVEL_LOW = 0x01, + OUT_PIN_LEVEL_HIGH = 0x00, +}; + +/* +Data Type: 6th byte +Target states: 9th byte + Moving target distance: 10~11th bytes + Moving target energy: 12th byte + Still target distance: 13~14th bytes + Still target energy: 15th byte + Detect distance: 16~17th bytes +*/ +enum PeriodicData : uint8_t { + DATA_TYPES = 6, + TARGET_STATES = 8, + MOVING_TARGET_LOW = 9, + MOVING_TARGET_HIGH = 10, + MOVING_ENERGY = 11, + STILL_TARGET_LOW = 12, + STILL_TARGET_HIGH = 13, + STILL_ENERGY = 14, + MOVING_SENSOR_START = 17, + STILL_SENSOR_START = 31, + LIGHT_SENSOR = 45, + OUT_PIN_SENSOR = 38, +}; + +enum PeriodicDataValue : uint8_t { + HEADER = 0XAA, + FOOTER = 0x55, + CHECK = 0x00, +}; + +enum AckData : uint8_t { + COMMAND = 6, + COMMAND_STATUS = 7, +}; + +// Memory-efficient lookup tables +struct StringToUint8 { + const char *str; + const uint8_t value; +}; + +struct Uint8ToString { + const uint8_t value; + const char *str; +}; + +constexpr StringToUint8 BAUD_RATES_BY_STR[] = { + {"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400}, + {"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, + {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, +}; + +constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = { + {"0.2m", DISTANCE_RESOLUTION_0_2}, + {"0.5m", DISTANCE_RESOLUTION_0_5}, + {"0.75m", DISTANCE_RESOLUTION_0_75}, +}; + +constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = { + {DISTANCE_RESOLUTION_0_2, "0.2m"}, + {DISTANCE_RESOLUTION_0_5, "0.5m"}, + {DISTANCE_RESOLUTION_0_75, "0.75m"}, +}; + +constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = { + {"off", LIGHT_FUNCTION_OFF}, + {"below", LIGHT_FUNCTION_BELOW}, + {"above", LIGHT_FUNCTION_ABOVE}, +}; + +constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = { + {LIGHT_FUNCTION_OFF, "off"}, + {LIGHT_FUNCTION_BELOW, "below"}, + {LIGHT_FUNCTION_ABOVE, "above"}, +}; + +constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = { + {"low", OUT_PIN_LEVEL_LOW}, + {"high", OUT_PIN_LEVEL_HIGH}, +}; + +constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { + {OUT_PIN_LEVEL_LOW, "low"}, + {OUT_PIN_LEVEL_HIGH, "high"}, +}; + +// Helper functions for lookups +template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { + for (const auto &entry : arr) { + if (str == entry.str) { + return entry.value; + } + } + return 0xFF; // Not found +} + +template const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { + for (const auto &entry : arr) { + if (value == entry.value) { + return entry.str; + } + } + return ""; // Not found +} + +static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Default used when number component is not defined +// Commands +static constexpr uint8_t CMD_ENABLE_CONF = 0xFF; +static constexpr uint8_t CMD_DISABLE_CONF = 0xFE; +static constexpr uint8_t CMD_ENABLE_ENG = 0x62; +static constexpr uint8_t CMD_DISABLE_ENG = 0x63; +static constexpr uint8_t CMD_QUERY_BASIC_CONF = 0x12; +static constexpr uint8_t CMD_BASIC_CONF = 0x02; +static constexpr uint8_t CMD_QUERY_VERSION = 0xA0; +static constexpr uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x11; +static constexpr uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x01; +static constexpr uint8_t CMD_QUERY_LIGHT_CONTROL = 0x1C; +static constexpr uint8_t CMD_SET_LIGHT_CONTROL = 0x0C; +static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1; +static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5; +static constexpr uint8_t CMD_FACTORY_RESET = 0xA2; +static constexpr uint8_t CMD_RESTART = 0xA3; +static constexpr uint8_t CMD_BLUETOOTH = 0xA4; +static constexpr uint8_t CMD_DYNAMIC_BACKGROUND_CORRECTION = 0x0B; +static constexpr uint8_t CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION = 0x1B; +static constexpr uint8_t CMD_MOTION_GATE_SENS = 0x03; +static constexpr uint8_t CMD_QUERY_MOTION_GATE_SENS = 0x13; +static constexpr uint8_t CMD_STATIC_GATE_SENS = 0x04; +static constexpr uint8_t CMD_QUERY_STATIC_GATE_SENS = 0x14; +static constexpr uint8_t CMD_NONE = 0x00; +// Commands values +static constexpr uint8_t CMD_MAX_MOVE_VALUE = 0x00; +static constexpr uint8_t CMD_MAX_STILL_VALUE = 0x01; +static constexpr uint8_t CMD_DURATION_VALUE = 0x02; +// Bitmasks for target states +static constexpr uint8_t MOVE_BITMASK = 0x01; +static constexpr uint8_t STILL_BITMASK = 0x02; +// Header & Footer size +static constexpr uint8_t HEADER_FOOTER_SIZE = 4; +// Command Header & Footer +static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA}; +static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01}; +// Data Header & Footer +static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1}; +static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5}; +// MAC address the module uses when Bluetooth is disabled +static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; + +static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } + +static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { + return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0; +} + +void LD2412Component::dump_config() { + std::string mac_str = + mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; + std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], + this->version_[4], this->version_[3], this->version_[2]); + ESP_LOGCONFIG(TAG, + "LD2412:\n" + " Firmware version: %s\n" + " MAC address: %s", + version.c_str(), mac_str.c_str()); +#ifdef USE_BINARY_SENSOR + ESP_LOGCONFIG(TAG, "Binary Sensors:"); + LOG_BINARY_SENSOR(" ", "DynamicBackgroundCorrectionStatus", + this->dynamic_background_correction_status_binary_sensor_); + LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "StillTarget", this->still_target_binary_sensor_); + LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); +#endif +#ifdef USE_SENSOR + ESP_LOGCONFIG(TAG, "Sensors:"); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "Light", this->light_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "DetectionDistance", this->detection_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetDistance", this->moving_target_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetEnergy", this->moving_target_energy_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetDistance", this->still_target_distance_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetEnergy", this->still_target_energy_sensor_); + for (auto &s : this->gate_still_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateStill", s); + } + for (auto &s : this->gate_move_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "GateMove", s); + } +#endif +#ifdef USE_TEXT_SENSOR + ESP_LOGCONFIG(TAG, "Text Sensors:"); + LOG_TEXT_SENSOR(" ", "MAC address", this->mac_text_sensor_); + LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_); +#endif +#ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, "Numbers:"); + LOG_NUMBER(" ", "LightThreshold", this->light_threshold_number_); + LOG_NUMBER(" ", "MaxDistanceGate", this->max_distance_gate_number_); + LOG_NUMBER(" ", "MinDistanceGate", this->min_distance_gate_number_); + LOG_NUMBER(" ", "Timeout", this->timeout_number_); + for (number::Number *n : this->gate_move_threshold_numbers_) { + LOG_NUMBER(" ", "Move Thresholds", n); + } + for (number::Number *n : this->gate_still_threshold_numbers_) { + LOG_NUMBER(" ", "Still Thresholds", n); + } +#endif +#ifdef USE_SELECT + ESP_LOGCONFIG(TAG, "Selects:"); + LOG_SELECT(" ", "BaudRate", this->baud_rate_select_); + LOG_SELECT(" ", "DistanceResolution", this->distance_resolution_select_); + LOG_SELECT(" ", "LightFunction", this->light_function_select_); + LOG_SELECT(" ", "OutPinLevel", this->out_pin_level_select_); +#endif +#ifdef USE_SWITCH + ESP_LOGCONFIG(TAG, "Switches:"); + LOG_SWITCH(" ", "Bluetooth", this->bluetooth_switch_); + LOG_SWITCH(" ", "EngineeringMode", this->engineering_mode_switch_); +#endif +#ifdef USE_BUTTON + ESP_LOGCONFIG(TAG, "Buttons:"); + LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_); + LOG_BUTTON(" ", "Query", this->query_button_); + LOG_BUTTON(" ", "Restart", this->restart_button_); + LOG_BUTTON(" ", "StartDynamicBackgroundCorrection", this->start_dynamic_background_correction_button_); +#endif +} + +void LD2412Component::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + this->read_all_info(); +} + +void LD2412Component::read_all_info() { + this->set_config_mode_(true); + this->get_version_(); + delay(10); // NOLINT + this->get_mac_(); + delay(10); // NOLINT + this->get_distance_resolution_(); + delay(10); // NOLINT + this->query_parameters_(); + delay(10); // NOLINT + this->query_dynamic_background_correction_(); + delay(10); // NOLINT + this->query_light_control_(); + delay(10); // NOLINT +#ifdef USE_NUMBER + this->get_gate_threshold(); + delay(10); // NOLINT +#endif + this->set_config_mode_(false); +#ifdef USE_SELECT + const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); + if (this->baud_rate_select_ != nullptr) { + this->baud_rate_select_->publish_state(baud_rate); + } +#endif +} + +void LD2412Component::restart_and_read_all_info() { + this->set_config_mode_(true); + this->restart_(); + this->set_timeout(1000, [this]() { this->read_all_info(); }); +} + +void LD2412Component::loop() { + while (this->available()) { + this->readline_(this->read()); + } +} + +void LD2412Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { + ESP_LOGV(TAG, "Sending COMMAND %02X", command); + // frame header bytes + this->write_array(CMD_FRAME_HEADER, HEADER_FOOTER_SIZE); + // length bytes + uint8_t len = 2; + if (command_value != nullptr) { + len += command_value_len; + } + // 2 length bytes (low, high) + 2 command bytes (low, high) + uint8_t len_cmd[] = {len, 0x00, command, 0x00}; + this->write_array(len_cmd, sizeof(len_cmd)); + + // command value bytes + if (command_value != nullptr) { + this->write_array(command_value, command_value_len); + } + // frame footer bytes + this->write_array(CMD_FRAME_FOOTER, HEADER_FOOTER_SIZE); + + if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) { + delay(30); // NOLINT + } + delay(20); // NOLINT +} + +void LD2412Component::handle_periodic_data_() { + // 4 frame header bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame footer bytes + // data header=0xAA, data footer=0x55, crc=0x00 + if (this->buffer_pos_ < 12 || !ld2412::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) || + this->buffer_data_[7] != HEADER || this->buffer_data_[this->buffer_pos_ - 6] != FOOTER) { + return; + } + /* + Data Type: 7th + 0x01: Engineering mode + 0x02: Normal mode + */ + bool engineering_mode = this->buffer_data_[DATA_TYPES] == 0x01; +#ifdef USE_SWITCH + if (this->engineering_mode_switch_ != nullptr) { + this->engineering_mode_switch_->publish_state(engineering_mode); + } +#endif + +#ifdef USE_BINARY_SENSOR + /* + Target states: 9th + 0x00 = No target + 0x01 = Moving targets + 0x02 = Still targets + 0x03 = Moving+Still targets + */ + char target_state = this->buffer_data_[TARGET_STATES]; + if (this->target_binary_sensor_ != nullptr) { + this->target_binary_sensor_->publish_state(target_state != 0x00); + } + if (this->moving_target_binary_sensor_ != nullptr) { + this->moving_target_binary_sensor_->publish_state(target_state & MOVE_BITMASK); + } + if (this->still_target_binary_sensor_ != nullptr) { + this->still_target_binary_sensor_->publish_state(target_state & STILL_BITMASK); + } +#endif + /* + Moving target distance: 10~11th bytes + Moving target energy: 12th byte + Still target distance: 13~14th bytes + Still target energy: 15th byte + Detect distance: 16~17th bytes + */ +#ifdef USE_SENSOR + SAFE_PUBLISH_SENSOR( + this->moving_target_distance_sensor_, + ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]) + SAFE_PUBLISH_SENSOR( + this->still_target_distance_sensor_, + ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH])) + SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]) + if (this->detection_distance_sensor_ != nullptr) { + int new_detect_distance = 0; + if (target_state != 0x00 && (target_state & MOVE_BITMASK)) { + new_detect_distance = + ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]); + } else if (target_state != 0x00) { + new_detect_distance = + ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]); + } + this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance); + } + if (engineering_mode) { + /* + Moving distance range: 18th byte + Still distance range: 19th byte + Moving energy: 20~28th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]) + } + /* + Still energy: 29~37th bytes + */ + for (uint8_t i = 0; i < TOTAL_GATES; i++) { + SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]) + } + /* + Light sensor: 38th bytes + */ + SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]) + } else { + for (auto &gate_move_sensor : this->gate_move_sensors_) { + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor) + } + for (auto &gate_still_sensor : this->gate_still_sensors_) { + SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor) + } + SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_) + } +#endif + // the radar module won't tell us when it's done, so we just have to keep polling... + if (this->dynamic_background_correction_active_) { + this->set_config_mode_(true); + this->query_dynamic_background_correction_(); + this->set_config_mode_(false); + } +} + +#ifdef USE_NUMBER +std::function set_number_value(number::Number *n, float value) { + if (n != nullptr && (!n->has_state() || n->state != value)) { + n->state = value; + return [n, value]() { n->publish_state(value); }; + } + return []() {}; +} +#endif + +bool LD2412Component::handle_ack_data_() { + ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]); + if (this->buffer_pos_ < 10) { + ESP_LOGW(TAG, "Invalid length"); + return true; + } + if (!ld2412::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); + return true; + } + if (this->buffer_data_[COMMAND_STATUS] != 0x01) { + ESP_LOGW(TAG, "Invalid status"); + return true; + } + if (this->buffer_data_[8] || this->buffer_data_[9]) { + ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]); + return true; + } + + switch (this->buffer_data_[COMMAND]) { + case CMD_ENABLE_CONF: + ESP_LOGV(TAG, "Enable conf"); + break; + + case CMD_DISABLE_CONF: + ESP_LOGV(TAG, "Disabled conf"); + break; + + case CMD_SET_BAUD_RATE: + ESP_LOGV(TAG, "Baud rate change"); +#ifdef USE_SELECT + if (this->baud_rate_select_ != nullptr) { + ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); + } +#endif + break; + + case CMD_QUERY_VERSION: { + std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); + std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], + this->version_[4], this->version_[3], this->version_[2]); + ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); +#ifdef USE_TEXT_SENSOR + if (this->version_text_sensor_ != nullptr) { + this->version_text_sensor_->publish_state(version); + } +#endif + break; + } + case CMD_QUERY_DISTANCE_RESOLUTION: { + const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]); + ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution); +#ifdef USE_SELECT + if (this->distance_resolution_select_ != nullptr) { + this->distance_resolution_select_->publish_state(distance_resolution); + } +#endif + break; + } + + case CMD_QUERY_LIGHT_CONTROL: { + this->light_function_ = this->buffer_data_[10]; + this->light_threshold_ = this->buffer_data_[11]; + const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); + ESP_LOGV(TAG, + "Light function: %s\n" + "Light threshold: %u", + light_function_str, this->light_threshold_); +#ifdef USE_SELECT + if (this->light_function_select_ != nullptr) { + this->light_function_select_->publish_state(light_function_str); + } +#endif +#ifdef USE_NUMBER + if (this->light_threshold_number_ != nullptr) { + this->light_threshold_number_->publish_state(static_cast(this->light_threshold_)); + } +#endif + break; + } + + case CMD_QUERY_MAC_ADDRESS: { + if (this->buffer_pos_ < 20) { + return false; + } + + this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0; + if (this->bluetooth_on_) { + std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); + } + + std::string mac_str = + mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; + ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); +#ifdef USE_TEXT_SENSOR + if (this->mac_text_sensor_ != nullptr) { + this->mac_text_sensor_->publish_state(mac_str); + } +#endif +#ifdef USE_SWITCH + if (this->bluetooth_switch_ != nullptr) { + this->bluetooth_switch_->publish_state(this->bluetooth_on_); + } +#endif + break; + } + + case CMD_SET_DISTANCE_RESOLUTION: + ESP_LOGV(TAG, "Handled set distance resolution command"); + break; + + case CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION: { + ESP_LOGV(TAG, "Handled query dynamic background correction"); + bool dynamic_background_correction_active = (this->buffer_data_[10] != 0x00); +#ifdef USE_BINARY_SENSOR + if (this->dynamic_background_correction_status_binary_sensor_ != nullptr) { + this->dynamic_background_correction_status_binary_sensor_->publish_state(dynamic_background_correction_active); + } +#endif + this->dynamic_background_correction_active_ = dynamic_background_correction_active; + break; + } + + case CMD_BLUETOOTH: + ESP_LOGV(TAG, "Handled bluetooth command"); + break; + + case CMD_SET_LIGHT_CONTROL: + ESP_LOGV(TAG, "Handled set light control command"); + break; + + case CMD_QUERY_MOTION_GATE_SENS: { +#ifdef USE_NUMBER + std::vector> updates; + updates.reserve(this->gate_still_threshold_numbers_.size()); + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { + updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[10 + i])); + } + for (auto &update : updates) { + update(); + } +#endif + break; + } + + case CMD_QUERY_STATIC_GATE_SENS: { +#ifdef USE_NUMBER + std::vector> updates; + updates.reserve(this->gate_still_threshold_numbers_.size()); + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { + updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[10 + i])); + } + for (auto &update : updates) { + update(); + } +#endif + break; + } + + case CMD_QUERY_BASIC_CONF: // Query parameters response + { +#ifdef USE_NUMBER + /* + Moving distance range: 9th byte + Still distance range: 10th byte + */ + std::vector> updates; + updates.push_back(set_number_value(this->min_distance_gate_number_, this->buffer_data_[10])); + updates.push_back(set_number_value(this->max_distance_gate_number_, this->buffer_data_[11] - 1)); + ESP_LOGV(TAG, "min_distance_gate_number_: %u, max_distance_gate_number_ %u", this->buffer_data_[10], + this->buffer_data_[11]); + /* + None Duration: 11~12th bytes + */ + updates.push_back(set_number_value(this->timeout_number_, + ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13]))); + ESP_LOGV(TAG, "timeout_number_: %u", ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13])); + /* + Output pin configuration: 13th bytes + */ + this->out_pin_level_ = this->buffer_data_[14]; +#ifdef USE_SELECT + const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_); + if (this->out_pin_level_select_ != nullptr) { + this->out_pin_level_select_->publish_state(out_pin_level_str); + } +#endif + for (auto &update : updates) { + update(); + } +#endif + } break; + default: + break; + } + + return true; +} + +void LD2412Component::readline_(int readch) { + if (readch < 0) { + return; // No data available + } + if (this->buffer_pos_ < HEADER_FOOTER_SIZE && readch != DATA_FRAME_HEADER[this->buffer_pos_] && + readch != CMD_FRAME_HEADER[this->buffer_pos_]) { + this->buffer_pos_ = 0; + return; + } + if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { + this->buffer_data_[this->buffer_pos_++] = readch; + this->buffer_data_[this->buffer_pos_] = 0; + } else { + // We should never get here, but just in case... + ESP_LOGW(TAG, "Max command length exceeded; ignoring"); + this->buffer_pos_ = 0; + } + if (this->buffer_pos_ < 4) { + return; // Not enough data to process yet + } + if (ld2412::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { + ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); + this->handle_periodic_data_(); + this->buffer_pos_ = 0; // Reset position index for next message + } else if (ld2412::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { + ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); + if (this->handle_ack_data_()) { + this->buffer_pos_ = 0; // Reset position index for next message + } else { + ESP_LOGV(TAG, "Ack Data incomplete"); + } + } +} + +void LD2412Component::set_config_mode_(bool enable) { + const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; + const uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value)); +} + +void LD2412Component::set_bluetooth(bool enable) { + this->set_config_mode_(true); + const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00}; + this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value)); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2412Component::set_distance_resolution(const std::string &state) { + this->set_config_mode_(true); + const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00, 0x00, 0x00, 0x00, 0x00}; + this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2412Component::set_baud_rate(const std::string &state) { + this->set_config_mode_(true); + const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; + this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); + this->set_timeout(200, [this]() { this->restart_(); }); +} + +void LD2412Component::query_dynamic_background_correction_() { + this->send_command_(CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION, nullptr, 0); +} + +void LD2412Component::start_dynamic_background_correction() { + if (this->dynamic_background_correction_active_) { + return; // Already in progress + } +#ifdef USE_BINARY_SENSOR + if (this->dynamic_background_correction_status_binary_sensor_ != nullptr) { + this->dynamic_background_correction_status_binary_sensor_->publish_state(true); + } +#endif + this->dynamic_background_correction_active_ = true; + this->set_config_mode_(true); + this->send_command_(CMD_DYNAMIC_BACKGROUND_CORRECTION, nullptr, 0); + this->set_config_mode_(false); +} + +void LD2412Component::set_engineering_mode(bool enable) { + const uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; + this->set_config_mode_(true); + this->send_command_(cmd, nullptr, 0); + this->set_config_mode_(false); +} + +void LD2412Component::factory_reset() { + this->set_config_mode_(true); + this->send_command_(CMD_FACTORY_RESET, nullptr, 0); + this->set_timeout(2000, [this]() { this->restart_and_read_all_info(); }); +} + +void LD2412Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } + +void LD2412Component::query_parameters_() { this->send_command_(CMD_QUERY_BASIC_CONF, nullptr, 0); } + +void LD2412Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); } + +void LD2412Component::get_mac_() { + const uint8_t cmd_value[2] = {0x01, 0x00}; + this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, sizeof(cmd_value)); +} + +void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY_DISTANCE_RESOLUTION, nullptr, 0); } + +void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); } + +void LD2412Component::set_basic_config() { +#ifdef USE_NUMBER + if (!this->min_distance_gate_number_->has_state() || !this->max_distance_gate_number_->has_state() || + !this->timeout_number_->has_state()) { + return; + } +#endif +#ifdef USE_SELECT + if (!this->out_pin_level_select_->has_state()) { + return; + } +#endif + + uint8_t value[5] = { +#ifdef USE_NUMBER + lowbyte(static_cast(this->min_distance_gate_number_->state)), + lowbyte(static_cast(this->max_distance_gate_number_->state) + 1), + lowbyte(static_cast(this->timeout_number_->state)), + highbyte(static_cast(this->timeout_number_->state)), +#else + 1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0, +#endif +#ifdef USE_SELECT + find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state), +#else + 0x01, // Default value if not using select +#endif + }; + this->set_config_mode_(true); + this->send_command_(CMD_BASIC_CONF, value, sizeof(value)); + this->set_config_mode_(false); +} + +#ifdef USE_NUMBER +void LD2412Component::set_gate_threshold() { + if (this->gate_move_threshold_numbers_.empty() && this->gate_still_threshold_numbers_.empty()) { + return; // No gate threshold numbers set; nothing to do here + } + uint8_t value[TOTAL_GATES] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + this->set_config_mode_(true); + if (!this->gate_move_threshold_numbers_.empty()) { + for (size_t i = 0; i < this->gate_move_threshold_numbers_.size(); i++) { + value[i] = lowbyte(static_cast(this->gate_move_threshold_numbers_[i]->state)); + } + this->send_command_(CMD_MOTION_GATE_SENS, value, sizeof(value)); + } + if (!this->gate_still_threshold_numbers_.empty()) { + for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) { + value[i] = lowbyte(static_cast(this->gate_still_threshold_numbers_[i]->state)); + } + this->send_command_(CMD_STATIC_GATE_SENS, value, sizeof(value)); + } + this->set_config_mode_(false); +} + +void LD2412Component::get_gate_threshold() { + this->send_command_(CMD_QUERY_MOTION_GATE_SENS, nullptr, 0); + this->send_command_(CMD_QUERY_STATIC_GATE_SENS, nullptr, 0); +} + +void LD2412Component::set_gate_still_threshold_number(uint8_t gate, number::Number *n) { + this->gate_still_threshold_numbers_[gate] = n; +} + +void LD2412Component::set_gate_move_threshold_number(uint8_t gate, number::Number *n) { + this->gate_move_threshold_numbers_[gate] = n; +} +#endif + +void LD2412Component::set_light_out_control() { +#ifdef USE_NUMBER + if (this->light_threshold_number_ != nullptr && this->light_threshold_number_->has_state()) { + this->light_threshold_ = static_cast(this->light_threshold_number_->state); + } +#endif +#ifdef USE_SELECT + if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { + this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state); + } +#endif + uint8_t value[2] = {this->light_function_, this->light_threshold_}; + this->set_config_mode_(true); + this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value)); + this->query_light_control_(); + this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); +} + +#ifdef USE_SENSOR +// These could leak memory, but they are only set once prior to 'setup()' and should never be used again. +void LD2412Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { + this->gate_move_sensors_[gate] = new SensorWithDedup(s); +} +void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { + this->gate_still_sensors_[gate] = new SensorWithDedup(s); +} +#endif + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/ld2412.h b/esphome/components/ld2412/ld2412.h new file mode 100644 index 0000000000..41f96ab301 --- /dev/null +++ b/esphome/components/ld2412/ld2412.h @@ -0,0 +1,141 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#include "esphome/components/ld24xx/ld24xx.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace ld2412 { + +using namespace ld24xx; + +static constexpr uint8_t MAX_LINE_LENGTH = 54; // Max characters for serial buffer +static constexpr uint8_t TOTAL_GATES = 14; // Total number of gates supported by the LD2412 + +class LD2412Component : public Component, public uart::UARTDevice { +#ifdef USE_BINARY_SENSOR + SUB_BINARY_SENSOR(dynamic_background_correction_status) + SUB_BINARY_SENSOR(moving_target) + SUB_BINARY_SENSOR(still_target) + SUB_BINARY_SENSOR(target) +#endif +#ifdef USE_SENSOR + SUB_SENSOR_WITH_DEDUP(light, uint8_t) + SUB_SENSOR_WITH_DEDUP(detection_distance, int) + SUB_SENSOR_WITH_DEDUP(moving_target_distance, int) + SUB_SENSOR_WITH_DEDUP(moving_target_energy, uint8_t) + SUB_SENSOR_WITH_DEDUP(still_target_distance, int) + SUB_SENSOR_WITH_DEDUP(still_target_energy, uint8_t) +#endif +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(mac) + SUB_TEXT_SENSOR(version) +#endif +#ifdef USE_NUMBER + SUB_NUMBER(light_threshold) + SUB_NUMBER(max_distance_gate) + SUB_NUMBER(min_distance_gate) + SUB_NUMBER(timeout) +#endif +#ifdef USE_SELECT + SUB_SELECT(baud_rate) + SUB_SELECT(distance_resolution) + SUB_SELECT(light_function) + SUB_SELECT(out_pin_level) +#endif +#ifdef USE_SWITCH + SUB_SWITCH(bluetooth) + SUB_SWITCH(engineering_mode) +#endif +#ifdef USE_BUTTON + SUB_BUTTON(factory_reset) + SUB_BUTTON(query) + SUB_BUTTON(restart) + SUB_BUTTON(start_dynamic_background_correction) +#endif + + public: + void setup() override; + void dump_config() override; + void loop() override; + void set_light_out_control(); + void set_basic_config(); +#ifdef USE_NUMBER + void set_gate_move_threshold_number(uint8_t gate, number::Number *n); + void set_gate_still_threshold_number(uint8_t gate, number::Number *n); + void set_gate_threshold(); + void get_gate_threshold(); +#endif +#ifdef USE_SENSOR + void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s); + void set_gate_still_sensor(uint8_t gate, sensor::Sensor *s); +#endif + void set_engineering_mode(bool enable); + void read_all_info(); + void restart_and_read_all_info(); + void set_bluetooth(bool enable); + void set_distance_resolution(const std::string &state); + void set_baud_rate(const std::string &state); + void factory_reset(); + void start_dynamic_background_correction(); + + protected: + void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); + void set_config_mode_(bool enable); + void handle_periodic_data_(); + bool handle_ack_data_(); + void readline_(int readch); + void query_parameters_(); + void get_version_(); + void get_mac_(); + void get_distance_resolution_(); + void query_light_control_(); + void restart_(); + void query_dynamic_background_correction_(); + + uint8_t light_function_ = 0; + uint8_t light_threshold_ = 0; + uint8_t out_pin_level_ = 0; + uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer + uint8_t buffer_data_[MAX_LINE_LENGTH]; + uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; + uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; + bool bluetooth_on_{false}; + bool dynamic_background_correction_active_{false}; +#ifdef USE_NUMBER + std::array gate_move_threshold_numbers_{}; + std::array gate_still_threshold_numbers_{}; +#endif +#ifdef USE_SENSOR + std::array *, TOTAL_GATES> gate_move_sensors_{}; + std::array *, TOTAL_GATES> gate_still_sensors_{}; +#endif +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/__init__.py b/esphome/components/ld2412/number/__init__.py new file mode 100644 index 0000000000..5b0d6d8749 --- /dev/null +++ b/esphome/components/ld2412/number/__init__.py @@ -0,0 +1,126 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MOVE_THRESHOLD, + CONF_STILL_THRESHOLD, + CONF_TIMEOUT, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_SIGNAL_STRENGTH, + ENTITY_CATEGORY_CONFIG, + ICON_LIGHTBULB, + ICON_MOTION_SENSOR, + ICON_TIMELAPSE, + UNIT_PERCENT, + UNIT_SECOND, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +GateThresholdNumber = LD2412_ns.class_("GateThresholdNumber", number.Number) +LightThresholdNumber = LD2412_ns.class_("LightThresholdNumber", number.Number) +MaxDistanceTimeoutNumber = LD2412_ns.class_("MaxDistanceTimeoutNumber", number.Number) + +CONF_LIGHT_THRESHOLD = "light_threshold" +CONF_MAX_DISTANCE_GATE = "max_distance_gate" +CONF_MIN_DISTANCE_GATE = "min_distance_gate" + +TIMEOUT_GROUP = "timeout" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema( + LightThresholdNumber, + device_class=DEVICE_CLASS_ILLUMINANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_LIGHTBULB, + ), + cv.Optional(CONF_MAX_DISTANCE_GATE): number.number_schema( + MaxDistanceTimeoutNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_MIN_DISTANCE_GATE): number.number_schema( + MaxDistanceTimeoutNumber, + device_class=DEVICE_CLASS_DISTANCE, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + ), + cv.Optional(CONF_TIMEOUT): number.number_schema( + MaxDistanceTimeoutNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_TIMELAPSE, + unit_of_measurement=UNIT_SECOND, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"gate_{x}"): ( + { + cv.Required(CONF_MOVE_THRESHOLD): number.number_schema( + GateThresholdNumber, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + cv.Required(CONF_STILL_THRESHOLD): number.number_schema( + GateThresholdNumber, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + } + ) + for x in range(14) + } +) + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if light_threshold_config := config.get(CONF_LIGHT_THRESHOLD): + n = await number.new_number( + light_threshold_config, min_value=0, max_value=255, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_light_threshold_number(n)) + if max_distance_gate_config := config.get(CONF_MAX_DISTANCE_GATE): + n = await number.new_number( + max_distance_gate_config, min_value=2, max_value=13, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_max_distance_gate_number(n)) + if min_distance_gate_config := config.get(CONF_MIN_DISTANCE_GATE): + n = await number.new_number( + min_distance_gate_config, min_value=1, max_value=12, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_min_distance_gate_number(n)) + for x in range(14): + if gate_conf := config.get(f"gate_{x}"): + move_config = gate_conf[CONF_MOVE_THRESHOLD] + n = cg.new_Pvariable(move_config[CONF_ID], x) + await number.register_number( + n, move_config, min_value=0, max_value=100, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_gate_move_threshold_number(x, n)) + still_config = gate_conf[CONF_STILL_THRESHOLD] + n = cg.new_Pvariable(still_config[CONF_ID], x) + await number.register_number( + n, still_config, min_value=0, max_value=100, step=1 + ) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_gate_still_threshold_number(x, n)) + if timeout_config := config.get(CONF_TIMEOUT): + n = await number.new_number(timeout_config, min_value=0, max_value=900, step=1) + await cg.register_parented(n, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_timeout_number(n)) diff --git a/esphome/components/ld2412/number/gate_threshold_number.cpp b/esphome/components/ld2412/number/gate_threshold_number.cpp new file mode 100644 index 0000000000..47f8cd9107 --- /dev/null +++ b/esphome/components/ld2412/number/gate_threshold_number.cpp @@ -0,0 +1,14 @@ +#include "gate_threshold_number.h" + +namespace esphome { +namespace ld2412 { + +GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} + +void GateThresholdNumber::control(float value) { + this->publish_state(value); + this->parent_->set_gate_threshold(); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/gate_threshold_number.h b/esphome/components/ld2412/number/gate_threshold_number.h new file mode 100644 index 0000000000..61d9945a0a --- /dev/null +++ b/esphome/components/ld2412/number/gate_threshold_number.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class GateThresholdNumber : public number::Number, public Parented { + public: + GateThresholdNumber(uint8_t gate); + + protected: + uint8_t gate_; + void control(float value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/light_threshold_number.cpp b/esphome/components/ld2412/number/light_threshold_number.cpp new file mode 100644 index 0000000000..5dab1716bf --- /dev/null +++ b/esphome/components/ld2412/number/light_threshold_number.cpp @@ -0,0 +1,12 @@ +#include "light_threshold_number.h" + +namespace esphome { +namespace ld2412 { + +void LightThresholdNumber::control(float value) { + this->publish_state(value); + this->parent_->set_light_out_control(); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/light_threshold_number.h b/esphome/components/ld2412/number/light_threshold_number.h new file mode 100644 index 0000000000..d8727d3c98 --- /dev/null +++ b/esphome/components/ld2412/number/light_threshold_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class LightThresholdNumber : public number::Number, public Parented { + public: + LightThresholdNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/max_distance_timeout_number.cpp b/esphome/components/ld2412/number/max_distance_timeout_number.cpp new file mode 100644 index 0000000000..1c6471bc72 --- /dev/null +++ b/esphome/components/ld2412/number/max_distance_timeout_number.cpp @@ -0,0 +1,12 @@ +#include "max_distance_timeout_number.h" + +namespace esphome { +namespace ld2412 { + +void MaxDistanceTimeoutNumber::control(float value) { + this->publish_state(value); + this->parent_->set_basic_config(); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/number/max_distance_timeout_number.h b/esphome/components/ld2412/number/max_distance_timeout_number.h new file mode 100644 index 0000000000..af0dcf68c5 --- /dev/null +++ b/esphome/components/ld2412/number/max_distance_timeout_number.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class MaxDistanceTimeoutNumber : public number::Number, public Parented { + public: + MaxDistanceTimeoutNumber() = default; + + protected: + void control(float value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/__init__.py b/esphome/components/ld2412/select/__init__.py new file mode 100644 index 0000000000..d71ce460d9 --- /dev/null +++ b/esphome/components/ld2412/select/__init__.py @@ -0,0 +1,82 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import ( + CONF_BAUD_RATE, + ENTITY_CATEGORY_CONFIG, + ICON_LIGHTBULB, + ICON_RULER, + ICON_SCALE, + ICON_THERMOMETER, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +BaudRateSelect = LD2412_ns.class_("BaudRateSelect", select.Select) +DistanceResolutionSelect = LD2412_ns.class_("DistanceResolutionSelect", select.Select) +LightOutControlSelect = LD2412_ns.class_("LightOutControlSelect", select.Select) + +CONF_DISTANCE_RESOLUTION = "distance_resolution" +CONF_LIGHT_FUNCTION = "light_function" +CONF_OUT_PIN_LEVEL = "out_pin_level" + + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_BAUD_RATE): select.select_schema( + BaudRateSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_THERMOMETER, + ), + cv.Optional(CONF_DISTANCE_RESOLUTION): select.select_schema( + DistanceResolutionSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RULER, + ), + cv.Optional(CONF_LIGHT_FUNCTION): select.select_schema( + LightOutControlSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_LIGHTBULB, + ), + cv.Optional(CONF_OUT_PIN_LEVEL): select.select_schema( + LightOutControlSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if baud_rate_config := config.get(CONF_BAUD_RATE): + s = await select.new_select( + baud_rate_config, + options=[ + "9600", + "19200", + "38400", + "57600", + "115200", + "230400", + "256000", + "460800", + ], + ) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_baud_rate_select(s)) + if distance_resolution_config := config.get(CONF_DISTANCE_RESOLUTION): + s = await select.new_select( + distance_resolution_config, options=["0.2m", "0.5m", "0.75m"] + ) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_distance_resolution_select(s)) + if light_function_config := config.get(CONF_LIGHT_FUNCTION): + s = await select.new_select( + light_function_config, options=["off", "below", "above"] + ) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_light_function_select(s)) + if out_pin_level_config := config.get(CONF_OUT_PIN_LEVEL): + s = await select.new_select(out_pin_level_config, options=["low", "high"]) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_out_pin_level_select(s)) diff --git a/esphome/components/ld2412/select/baud_rate_select.cpp b/esphome/components/ld2412/select/baud_rate_select.cpp new file mode 100644 index 0000000000..2291a81896 --- /dev/null +++ b/esphome/components/ld2412/select/baud_rate_select.cpp @@ -0,0 +1,12 @@ +#include "baud_rate_select.h" + +namespace esphome { +namespace ld2412 { + +void BaudRateSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_baud_rate(state); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/baud_rate_select.h b/esphome/components/ld2412/select/baud_rate_select.h new file mode 100644 index 0000000000..2ae33551fb --- /dev/null +++ b/esphome/components/ld2412/select/baud_rate_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class BaudRateSelect : public select::Select, public Parented { + public: + BaudRateSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/distance_resolution_select.cpp b/esphome/components/ld2412/select/distance_resolution_select.cpp new file mode 100644 index 0000000000..a282215fbd --- /dev/null +++ b/esphome/components/ld2412/select/distance_resolution_select.cpp @@ -0,0 +1,12 @@ +#include "distance_resolution_select.h" + +namespace esphome { +namespace ld2412 { + +void DistanceResolutionSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_distance_resolution(state); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/distance_resolution_select.h b/esphome/components/ld2412/select/distance_resolution_select.h new file mode 100644 index 0000000000..0658f5d1a7 --- /dev/null +++ b/esphome/components/ld2412/select/distance_resolution_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class DistanceResolutionSelect : public select::Select, public Parented { + public: + DistanceResolutionSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/light_out_control_select.cpp b/esphome/components/ld2412/select/light_out_control_select.cpp new file mode 100644 index 0000000000..c331729d40 --- /dev/null +++ b/esphome/components/ld2412/select/light_out_control_select.cpp @@ -0,0 +1,12 @@ +#include "light_out_control_select.h" + +namespace esphome { +namespace ld2412 { + +void LightOutControlSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_light_out_control(); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/select/light_out_control_select.h b/esphome/components/ld2412/select/light_out_control_select.h new file mode 100644 index 0000000000..a71bab1e14 --- /dev/null +++ b/esphome/components/ld2412/select/light_out_control_select.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class LightOutControlSelect : public select::Select, public Parented { + public: + LightOutControlSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py new file mode 100644 index 0000000000..abb823faad --- /dev/null +++ b/esphome/components/ld2412/sensor.py @@ -0,0 +1,124 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_LIGHT, + CONF_MOVING_DISTANCE, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ILLUMINANCE, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_FLASH, + ICON_LIGHTBULB, + ICON_MOTION_SENSOR, + ICON_SIGNAL, + UNIT_CENTIMETER, + UNIT_EMPTY, + UNIT_PERCENT, +) + +from . import CONF_LD2412_ID, LD2412Component + +DEPENDENCIES = ["ld2412"] + +CONF_DETECTION_DISTANCE = "detection_distance" +CONF_MOVE_ENERGY = "move_energy" +CONF_MOVING_ENERGY = "moving_energy" +CONF_STILL_DISTANCE = "still_distance" +CONF_STILL_ENERGY = "still_energy" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, + ), + cv.Optional(CONF_LIGHT): sensor.sensor_schema( + device_class=DEVICE_CLASS_ILLUMINANCE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_LIGHTBULB, + unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor + ), + cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, + ), + cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( + device_class=DEVICE_CLASS_DISTANCE, + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_SIGNAL, + unit_of_measurement=UNIT_CENTIMETER, + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + icon=ICON_FLASH, + unit_of_measurement=UNIT_PERCENT, + ), + } +) + +CONFIG_SCHEMA = CONFIG_SCHEMA.extend( + { + cv.Optional(f"gate_{x}"): ( + { + cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + ], + icon=ICON_MOTION_SENSOR, + unit_of_measurement=UNIT_PERCENT, + ), + cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + filters=[ + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + ], + icon=ICON_FLASH, + unit_of_measurement=UNIT_PERCENT, + ), + } + ) + for x in range(14) + } +) + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if detection_distance_config := config.get(CONF_DETECTION_DISTANCE): + sens = await sensor.new_sensor(detection_distance_config) + cg.add(LD2412_component.set_detection_distance_sensor(sens)) + if light_config := config.get(CONF_LIGHT): + sens = await sensor.new_sensor(light_config) + cg.add(LD2412_component.set_light_sensor(sens)) + if moving_distance_config := config.get(CONF_MOVING_DISTANCE): + sens = await sensor.new_sensor(moving_distance_config) + cg.add(LD2412_component.set_moving_target_distance_sensor(sens)) + if moving_energy_config := config.get(CONF_MOVING_ENERGY): + sens = await sensor.new_sensor(moving_energy_config) + cg.add(LD2412_component.set_moving_target_energy_sensor(sens)) + if still_distance_config := config.get(CONF_STILL_DISTANCE): + sens = await sensor.new_sensor(still_distance_config) + cg.add(LD2412_component.set_still_target_distance_sensor(sens)) + if still_energy_config := config.get(CONF_STILL_ENERGY): + sens = await sensor.new_sensor(still_energy_config) + cg.add(LD2412_component.set_still_target_energy_sensor(sens)) + for x in range(14): + if gate_conf := config.get(f"gate_{x}"): + if move_config := gate_conf.get(CONF_MOVE_ENERGY): + sens = await sensor.new_sensor(move_config) + cg.add(LD2412_component.set_gate_move_sensor(x, sens)) + if still_config := gate_conf.get(CONF_STILL_ENERGY): + sens = await sensor.new_sensor(still_config) + cg.add(LD2412_component.set_gate_still_sensor(x, sens)) diff --git a/esphome/components/ld2412/switch/__init__.py b/esphome/components/ld2412/switch/__init__.py new file mode 100644 index 0000000000..df994687ec --- /dev/null +++ b/esphome/components/ld2412/switch/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import ( + CONF_BLUETOOTH, + DEVICE_CLASS_SWITCH, + ENTITY_CATEGORY_CONFIG, + ICON_BLUETOOTH, + ICON_PULSE, +) + +from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component + +BluetoothSwitch = LD2412_ns.class_("BluetoothSwitch", switch.Switch) +EngineeringModeSwitch = LD2412_ns.class_("EngineeringModeSwitch", switch.Switch) + +CONF_ENGINEERING_MODE = "engineering_mode" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_BLUETOOTH): switch.switch_schema( + BluetoothSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_BLUETOOTH, + ), + cv.Optional(CONF_ENGINEERING_MODE): switch.switch_schema( + EngineeringModeSwitch, + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_PULSE, + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if bluetooth_config := config.get(CONF_BLUETOOTH): + s = await switch.new_switch(bluetooth_config) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_bluetooth_switch(s)) + if engineering_mode_config := config.get(CONF_ENGINEERING_MODE): + s = await switch.new_switch(engineering_mode_config) + await cg.register_parented(s, config[CONF_LD2412_ID]) + cg.add(LD2412_component.set_engineering_mode_switch(s)) diff --git a/esphome/components/ld2412/switch/bluetooth_switch.cpp b/esphome/components/ld2412/switch/bluetooth_switch.cpp new file mode 100644 index 0000000000..14387aa276 --- /dev/null +++ b/esphome/components/ld2412/switch/bluetooth_switch.cpp @@ -0,0 +1,12 @@ +#include "bluetooth_switch.h" + +namespace esphome { +namespace ld2412 { + +void BluetoothSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_bluetooth(state); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/switch/bluetooth_switch.h b/esphome/components/ld2412/switch/bluetooth_switch.h new file mode 100644 index 0000000000..730d338d87 --- /dev/null +++ b/esphome/components/ld2412/switch/bluetooth_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class BluetoothSwitch : public switch_::Switch, public Parented { + public: + BluetoothSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.cpp b/esphome/components/ld2412/switch/engineering_mode_switch.cpp new file mode 100644 index 0000000000..29ca0c22a8 --- /dev/null +++ b/esphome/components/ld2412/switch/engineering_mode_switch.cpp @@ -0,0 +1,12 @@ +#include "engineering_mode_switch.h" + +namespace esphome { +namespace ld2412 { + +void EngineeringModeSwitch::write_state(bool state) { + this->publish_state(state); + this->parent_->set_engineering_mode(state); +} + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.h b/esphome/components/ld2412/switch/engineering_mode_switch.h new file mode 100644 index 0000000000..aaa404c673 --- /dev/null +++ b/esphome/components/ld2412/switch/engineering_mode_switch.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "../ld2412.h" + +namespace esphome { +namespace ld2412 { + +class EngineeringModeSwitch : public switch_::Switch, public Parented { + public: + EngineeringModeSwitch() = default; + + protected: + void write_state(bool state) override; +}; + +} // namespace ld2412 +} // namespace esphome diff --git a/esphome/components/ld2412/text_sensor.py b/esphome/components/ld2412/text_sensor.py new file mode 100644 index 0000000000..1074494933 --- /dev/null +++ b/esphome/components/ld2412/text_sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_MAC_ADDRESS, + CONF_VERSION, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_BLUETOOTH, + ICON_CHIP, +) + +from . import CONF_LD2412_ID, LD2412Component + +DEPENDENCIES = ["ld2412"] + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP + ), + cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_BLUETOOTH + ), +} + + +async def to_code(config): + LD2412_component = await cg.get_variable(config[CONF_LD2412_ID]) + if version_config := config.get(CONF_VERSION): + sens = await text_sensor.new_text_sensor(version_config) + cg.add(LD2412_component.set_version_text_sensor(sens)) + if mac_address_config := config.get(CONF_MAC_ADDRESS): + sens = await text_sensor.new_text_sensor(mac_address_config) + cg.add(LD2412_component.set_mac_text_sensor(sens)) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 0baff368c8..3842098c44 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -213,7 +213,6 @@ void LD2420Component::dump_config() { } void LD2420Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); this->mark_failed(); diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index 442fdaa125..bd6d697c90 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -3,6 +3,7 @@ from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_THROTTLE +AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] CODEOWNERS = ["@hareeshmu"] MULTI_CONF = True @@ -16,9 +17,8 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(LD2450Component), - cv.Optional(CONF_THROTTLE, default="1000ms"): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(min=cv.TimePeriod(milliseconds=1)), + cv.Optional(CONF_THROTTLE): cv.invalid( + f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" ), } ) @@ -45,4 +45,3 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) - cg.add(var.set_throttle(config[CONF_THROTTLE])) diff --git a/esphome/components/ld2450/binary_sensor.py b/esphome/components/ld2450/binary_sensor.py index d0082ac21a..37f722b0fa 100644 --- a/esphome/components/ld2450/binary_sensor.py +++ b/esphome/components/ld2450/binary_sensor.py @@ -21,14 +21,17 @@ CONFIG_SCHEMA = { cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_SHIELD_ACCOUNT, ), cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_MOTION, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_TARGET_ACCOUNT, ), cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema( device_class=DEVICE_CLASS_OCCUPANCY, + filters=[{"settle": cv.TimePeriod(milliseconds=1000)}], icon=ICON_MEDITATION, ), } diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 09761b2937..b123d541d9 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -1,6 +1,5 @@ #include "ld2450.h" -#include -#include + #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif @@ -11,8 +10,8 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#define highbyte(val) (uint8_t)((val) >> 8) -#define lowbyte(val) (uint8_t)((val) &0xff) +#include +#include namespace esphome { namespace ld2450 { @@ -170,25 +169,19 @@ static inline int16_t hex_to_signed_int(const uint8_t *buffer, uint8_t offset) { } static inline float calculate_angle(float base, float hypotenuse) { - if (base < 0.0 || hypotenuse <= 0.0) { - return 0.0; + if (base < 0.0f || hypotenuse <= 0.0f) { + return 0.0f; } - float angle_radians = std::acos(base / hypotenuse); - float angle_degrees = angle_radians * (180.0 / M_PI); + float angle_radians = acosf(base / hypotenuse); + float angle_degrees = angle_radians * (180.0f / std::numbers::pi_v); return angle_degrees; } -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 LD2450Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); #ifdef USE_NUMBER if (this->presence_timeout_number_ != nullptr) { this->pref_ = global_preferences->make_preference(this->presence_timeout_number_->get_object_id_hash()); @@ -206,9 +199,8 @@ void LD2450Component::dump_config() { ESP_LOGCONFIG(TAG, "LD2450:\n" " Firmware version: %s\n" - " MAC address: %s\n" - " Throttle: %u ms", - version.c_str(), mac_str.c_str(), this->throttle_); + " MAC address: %s", + version.c_str(), mac_str.c_str()); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); @@ -217,41 +209,41 @@ void LD2450Component::dump_config() { #endif #ifdef USE_SENSOR ESP_LOGCONFIG(TAG, "Sensors:"); - LOG_SENSOR(" ", "MovingTargetCount", this->moving_target_count_sensor_); - LOG_SENSOR(" ", "StillTargetCount", this->still_target_count_sensor_); - LOG_SENSOR(" ", "TargetCount", this->target_count_sensor_); - for (sensor::Sensor *s : this->move_x_sensors_) { - LOG_SENSOR(" ", "TargetX", s); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "MovingTargetCount", this->moving_target_count_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "StillTargetCount", this->still_target_count_sensor_); + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetCount", this->target_count_sensor_); + for (auto &s : this->move_x_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetX", s); } - for (sensor::Sensor *s : this->move_y_sensors_) { - LOG_SENSOR(" ", "TargetY", s); + for (auto &s : this->move_y_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetY", s); } - for (sensor::Sensor *s : this->move_angle_sensors_) { - LOG_SENSOR(" ", "TargetAngle", s); + for (auto &s : this->move_angle_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetAngle", s); } - for (sensor::Sensor *s : this->move_distance_sensors_) { - LOG_SENSOR(" ", "TargetDistance", s); + for (auto &s : this->move_distance_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetDistance", s); } - for (sensor::Sensor *s : this->move_resolution_sensors_) { - LOG_SENSOR(" ", "TargetResolution", s); + for (auto &s : this->move_resolution_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetResolution", s); } - for (sensor::Sensor *s : this->move_speed_sensors_) { - LOG_SENSOR(" ", "TargetSpeed", s); + for (auto &s : this->move_speed_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "TargetSpeed", s); } - for (sensor::Sensor *s : this->zone_target_count_sensors_) { - LOG_SENSOR(" ", "ZoneTargetCount", s); + for (auto &s : this->zone_target_count_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "ZoneTargetCount", s); } - for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) { - LOG_SENSOR(" ", "ZoneMovingTargetCount", s); + for (auto &s : this->zone_moving_target_count_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "ZoneMovingTargetCount", s); } - for (sensor::Sensor *s : this->zone_still_target_count_sensors_) { - LOG_SENSOR(" ", "ZoneStillTargetCount", s); + for (auto &s : this->zone_still_target_count_sensors_) { + LOG_SENSOR_WITH_DEDUP_SAFE(" ", "ZoneStillTargetCount", s); } #endif #ifdef USE_TEXT_SENSOR ESP_LOGCONFIG(TAG, "Text Sensors:"); LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_); - LOG_TEXT_SENSOR(" ", "Mac", this->mac_text_sensor_); + LOG_TEXT_SENSOR(" ", "MAC address", this->mac_text_sensor_); for (text_sensor::TextSensor *s : this->direction_text_sensors_) { LOG_TEXT_SENSOR(" ", "Direction", s); } @@ -419,30 +411,25 @@ void LD2450Component::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)); - // FIXME to remove - delay(50); // NOLINT + + if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) { + delay(50); // NOLINT + } } // LD2450 Radar data message: // [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC] // Header Target 1 Target 2 Target 3 End void LD2450Component::handle_periodic_data_() { - // Early throttle check - moved before any processing to save CPU cycles - if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { - return; - } - if (this->buffer_pos_ < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes) ESP_LOGE(TAG, "Invalid length"); return; @@ -453,14 +440,12 @@ void LD2450Component::handle_periodic_data_() { ESP_LOGE(TAG, "Invalid header/footer"); return; } - // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately - this->last_periodic_millis_ = App.get_loop_component_start_time(); int16_t target_count = 0; int16_t still_target_count = 0; int16_t moving_target_count = 0; + int16_t res = 0; int16_t start = 0; - int16_t val = 0; int16_t tx = 0; int16_t ty = 0; int16_t td = 0; @@ -478,85 +463,43 @@ void LD2450Component::handle_periodic_data_() { start = TARGET_X + index * 8; is_moving = false; // tx is used for further calculations, so always needs to be populated - val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); - tx = val; - sensor::Sensor *sx = this->move_x_sensors_[index]; - if (sx != nullptr) { - if (this->cached_target_data_[index].x != val) { - sx->publish_state(val); - this->cached_target_data_[index].x = val; - } - } + tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); + SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], tx); // Y start = TARGET_Y + index * 8; - // ty is used for further calculations, so always needs to be populated - val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); - ty = val; - sensor::Sensor *sy = this->move_y_sensors_[index]; - if (sy != nullptr) { - if (this->cached_target_data_[index].y != val) { - sy->publish_state(val); - this->cached_target_data_[index].y = val; - } - } + ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]); + SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], ty); // RESOLUTION start = TARGET_RESOLUTION + index * 8; - sensor::Sensor *sr = this->move_resolution_sensors_[index]; - if (sr != nullptr) { - val = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start]; - if (this->cached_target_data_[index].resolution != val) { - sr->publish_state(val); - this->cached_target_data_[index].resolution = val; - } - } + res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start]; + SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], res); #endif // SPEED start = TARGET_SPEED + index * 8; - val = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]); - ts = val; - if (val) { + ts = ld2450::decode_speed(this->buffer_data_[start], this->buffer_data_[start + 1]); + if (ts) { is_moving = true; moving_target_count++; } #ifdef USE_SENSOR - sensor::Sensor *ss = this->move_speed_sensors_[index]; - if (ss != nullptr) { - if (this->cached_target_data_[index].speed != val) { - ss->publish_state(val); - this->cached_target_data_[index].speed = val; - } - } + SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], ts); #endif // DISTANCE // Optimized: use already decoded tx and ty values, replace pow() with multiplication int32_t x_squared = (int32_t) tx * tx; int32_t y_squared = (int32_t) ty * ty; - val = (uint16_t) sqrt(x_squared + y_squared); - td = val; - if (val > 0) { + td = (uint16_t) sqrtf(x_squared + y_squared); + if (td > 0) { target_count++; } #ifdef USE_SENSOR - sensor::Sensor *sd = this->move_distance_sensors_[index]; - if (sd != nullptr) { - if (this->cached_target_data_[index].distance != val) { - sd->publish_state(val); - this->cached_target_data_[index].distance = val; - } - } + SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td); // ANGLE angle = ld2450::calculate_angle(static_cast(ty), static_cast(td)); if (tx > 0) { angle = angle * -1; } - sensor::Sensor *sa = this->move_angle_sensors_[index]; - if (sa != nullptr) { - if (std::isnan(this->cached_target_data_[index].angle) || - std::abs(this->cached_target_data_[index].angle - angle) > 0.1f) { - sa->publish_state(angle); - this->cached_target_data_[index].angle = angle; - } - } + SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle); #endif #ifdef USE_TEXT_SENSOR // DIRECTION @@ -570,11 +513,9 @@ void LD2450Component::handle_periodic_data_() { direction = DIRECTION_STATIONARY; } text_sensor::TextSensor *tsd = this->direction_text_sensors_[index]; - if (tsd != nullptr) { - if (this->cached_target_data_[index].direction != direction) { - tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction)); - this->cached_target_data_[index].direction = direction; - } + const auto *dir_str = find_str(ld2450::DIRECTION_BY_UINT, direction); + if (tsd != nullptr && (!tsd->has_state() || tsd->get_state() != dir_str)) { + tsd->publish_state(dir_str); } #endif @@ -599,53 +540,19 @@ void LD2450Component::handle_periodic_data_() { zone_all_targets = zone_still_targets + zone_moving_targets; // Publish Still Target Count in Zones - sensor::Sensor *szstc = this->zone_still_target_count_sensors_[index]; - if (szstc != nullptr) { - if (this->cached_zone_data_[index].still_count != zone_still_targets) { - szstc->publish_state(zone_still_targets); - this->cached_zone_data_[index].still_count = zone_still_targets; - } - } + SAFE_PUBLISH_SENSOR(this->zone_still_target_count_sensors_[index], zone_still_targets); // Publish Moving Target Count in Zones - sensor::Sensor *szmtc = this->zone_moving_target_count_sensors_[index]; - if (szmtc != nullptr) { - if (this->cached_zone_data_[index].moving_count != zone_moving_targets) { - szmtc->publish_state(zone_moving_targets); - this->cached_zone_data_[index].moving_count = zone_moving_targets; - } - } + SAFE_PUBLISH_SENSOR(this->zone_moving_target_count_sensors_[index], zone_moving_targets); // Publish All Target Count in Zones - sensor::Sensor *sztc = this->zone_target_count_sensors_[index]; - if (sztc != nullptr) { - if (this->cached_zone_data_[index].total_count != zone_all_targets) { - sztc->publish_state(zone_all_targets); - this->cached_zone_data_[index].total_count = zone_all_targets; - } - } - + SAFE_PUBLISH_SENSOR(this->zone_target_count_sensors_[index], zone_all_targets); } // End loop thru zones // Target Count - if (this->target_count_sensor_ != nullptr) { - if (this->cached_global_data_.target_count != target_count) { - this->target_count_sensor_->publish_state(target_count); - this->cached_global_data_.target_count = target_count; - } - } + SAFE_PUBLISH_SENSOR(this->target_count_sensor_, target_count); // Still Target Count - if (this->still_target_count_sensor_ != nullptr) { - if (this->cached_global_data_.still_count != still_target_count) { - this->still_target_count_sensor_->publish_state(still_target_count); - this->cached_global_data_.still_count = still_target_count; - } - } + SAFE_PUBLISH_SENSOR(this->still_target_count_sensor_, still_target_count); // Moving Target Count - if (this->moving_target_count_sensor_ != nullptr) { - if (this->cached_global_data_.moving_count != moving_target_count) { - this->moving_target_count_sensor_->publish_state(moving_target_count); - this->cached_global_data_.moving_count = moving_target_count; - } - } + SAFE_PUBLISH_SENSOR(this->moving_target_count_sensor_, moving_target_count); #endif #ifdef USE_BINARY_SENSOR @@ -942,28 +849,33 @@ void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QU void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); } #ifdef USE_SENSOR -void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) { this->move_x_sensors_[target] = s; } -void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) { this->move_y_sensors_[target] = s; } +// These could leak memory, but they are only set once prior to 'setup()' and should never be used again. +void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) { + this->move_x_sensors_[target] = new SensorWithDedup(s); +} +void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) { + this->move_y_sensors_[target] = new SensorWithDedup(s); +} void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) { - this->move_speed_sensors_[target] = s; + this->move_speed_sensors_[target] = new SensorWithDedup(s); } void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) { - this->move_angle_sensors_[target] = s; + this->move_angle_sensors_[target] = new SensorWithDedup(s); } void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) { - this->move_distance_sensors_[target] = s; + this->move_distance_sensors_[target] = new SensorWithDedup(s); } void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) { - this->move_resolution_sensors_[target] = s; + this->move_resolution_sensors_[target] = new SensorWithDedup(s); } void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) { - this->zone_target_count_sensors_[zone] = s; + this->zone_target_count_sensors_[zone] = new SensorWithDedup(s); } void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) { - this->zone_still_target_count_sensors_[zone] = s; + this->zone_still_target_count_sensors_[zone] = new SensorWithDedup(s); } void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) { - this->zone_moving_target_count_sensors_[zone] = s; + this->zone_moving_target_count_sensors_[zone] = new SensorWithDedup(s); } #endif #ifdef USE_TEXT_SENSOR diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index ae72a0d8cb..9faa189019 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,12 +1,7 @@ #pragma once -#include "esphome/components/uart/uart.h" -#include "esphome/core/component.h" #include "esphome/core/defines.h" -#include "esphome/core/helpers.h" -#include "esphome/core/preferences.h" -#include -#include +#include "esphome/core/component.h" #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif @@ -29,18 +24,23 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #endif -#ifndef M_PI -#define M_PI 3.14 -#endif +#include "esphome/components/ld24xx/ld24xx.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" + +#include namespace esphome { namespace ld2450 { +using namespace ld24xx; + // Constants -static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. -static const uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer -static const uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 -static const uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 +static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec. +static constexpr uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer +static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450 +static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450 enum Direction : uint8_t { DIRECTION_APPROACHING = 0, @@ -81,9 +81,9 @@ class LD2450Component : public Component, public uart::UARTDevice { SUB_BINARY_SENSOR(target) #endif #ifdef USE_SENSOR - SUB_SENSOR(moving_target_count) - SUB_SENSOR(still_target_count) - SUB_SENSOR(target_count) + SUB_SENSOR_WITH_DEDUP(moving_target_count, uint8_t) + SUB_SENSOR_WITH_DEDUP(still_target_count, uint8_t) + SUB_SENSOR_WITH_DEDUP(target_count, uint8_t) #endif #ifdef USE_TEXT_SENSOR SUB_TEXT_SENSOR(mac) @@ -110,7 +110,6 @@ class LD2450Component : public Component, public uart::UARTDevice { void dump_config() override; void loop() override; void set_presence_timeout(); - void set_throttle(uint16_t value) { this->throttle_ = value; } void read_all_info(); void query_zone_info(); void restart_and_read_all_info(); @@ -161,11 +160,9 @@ class LD2450Component : public Component, public uart::UARTDevice { bool get_timeout_status_(uint32_t check_millis); uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); - uint32_t last_periodic_millis_ = 0; uint32_t presence_millis_ = 0; uint32_t still_presence_millis_ = 0; uint32_t moving_presence_millis_ = 0; - uint16_t throttle_ = 0; uint16_t timeout_ = 5; uint8_t buffer_data_[MAX_LINE_LENGTH]; uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; @@ -176,48 +173,23 @@ class LD2450Component : public Component, public uart::UARTDevice { Target target_info_[MAX_TARGETS]; Zone zone_config_[MAX_ZONES]; - // Change detection - cache previous values to avoid redundant publishes - // All values are initialized to sentinel values that are outside the valid sensor ranges - // to ensure the first real measurement is always published - struct CachedTargetData { - int16_t x = std::numeric_limits::min(); // -32768, outside range of -4860 to 4860 - int16_t y = std::numeric_limits::min(); // -32768, outside range of 0 to 7560 - int16_t speed = std::numeric_limits::min(); // -32768, outside practical sensor range - uint16_t resolution = std::numeric_limits::max(); // 65535, unlikely resolution value - uint16_t distance = std::numeric_limits::max(); // 65535, outside range of 0 to ~8990 - Direction direction = DIRECTION_UNDEFINED; // Undefined, will differ from any real direction - float angle = NAN; // NAN, safe sentinel for floats - } cached_target_data_[MAX_TARGETS]; - - struct CachedZoneData { - uint8_t still_count = std::numeric_limits::max(); // 255, unlikely zone count - uint8_t moving_count = std::numeric_limits::max(); // 255, unlikely zone count - uint8_t total_count = std::numeric_limits::max(); // 255, unlikely zone count - } cached_zone_data_[MAX_ZONES]; - - struct CachedGlobalData { - uint8_t target_count = std::numeric_limits::max(); // 255, max 3 targets possible - uint8_t still_count = std::numeric_limits::max(); // 255, max 3 targets possible - uint8_t moving_count = std::numeric_limits::max(); // 255, max 3 targets possible - } cached_global_data_; - #ifdef USE_NUMBER ESPPreferenceObject pref_; // only used when numbers are in use ZoneOfNumbers zone_numbers_[MAX_ZONES]; #endif #ifdef USE_SENSOR - std::vector move_x_sensors_ = std::vector(MAX_TARGETS); - std::vector move_y_sensors_ = std::vector(MAX_TARGETS); - std::vector move_speed_sensors_ = std::vector(MAX_TARGETS); - std::vector move_angle_sensors_ = std::vector(MAX_TARGETS); - std::vector move_distance_sensors_ = std::vector(MAX_TARGETS); - std::vector move_resolution_sensors_ = std::vector(MAX_TARGETS); - std::vector zone_target_count_sensors_ = std::vector(MAX_ZONES); - std::vector zone_still_target_count_sensors_ = std::vector(MAX_ZONES); - std::vector zone_moving_target_count_sensors_ = std::vector(MAX_ZONES); + std::array *, MAX_TARGETS> move_x_sensors_{}; + std::array *, MAX_TARGETS> move_y_sensors_{}; + std::array *, MAX_TARGETS> move_speed_sensors_{}; + std::array *, MAX_TARGETS> move_angle_sensors_{}; + std::array *, MAX_TARGETS> move_distance_sensors_{}; + std::array *, MAX_TARGETS> move_resolution_sensors_{}; + std::array *, MAX_ZONES> zone_target_count_sensors_{}; + std::array *, MAX_ZONES> zone_still_target_count_sensors_{}; + std::array *, MAX_ZONES> zone_moving_target_count_sensors_{}; #endif #ifdef USE_TEXT_SENSOR - std::vector direction_text_sensors_ = std::vector(3); + std::array direction_text_sensors_{}; #endif }; diff --git a/esphome/components/ld2450/sensor.py b/esphome/components/ld2450/sensor.py index 071ce8aa32..4a3597d583 100644 --- a/esphome/components/ld2450/sensor.py +++ b/esphome/components/ld2450/sensor.py @@ -42,12 +42,42 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component), cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( + accuracy_decimals=0, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_ACCOUNT_GROUP, ), cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( + accuracy_decimals=0, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_HUMAN_GREETING_PROXIMITY, ), cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( + accuracy_decimals=0, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_ACCOUNT_SWITCH, ), } @@ -59,32 +89,86 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( { cv.Optional(CONF_X): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_MILLIMETER, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_ALPHA_X_BOX_OUTLINE, + unit_of_measurement=UNIT_MILLIMETER, ), cv.Optional(CONF_Y): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_MILLIMETER, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_ALPHA_Y_BOX_OUTLINE, + unit_of_measurement=UNIT_MILLIMETER, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( device_class=DEVICE_CLASS_SPEED, - unit_of_measurement=UNIT_MILLIMETER_PER_SECOND, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SPEEDOMETER_SLOW, + unit_of_measurement=UNIT_MILLIMETER_PER_SECOND, ), cv.Optional(CONF_ANGLE): sensor.sensor_schema( - unit_of_measurement=UNIT_DEGREES, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP, + unit_of_measurement=UNIT_DEGREES, ), cv.Optional(CONF_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_MILLIMETER, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MAP_MARKER_DISTANCE, + unit_of_measurement=UNIT_MILLIMETER, ), cv.Optional(CONF_RESOLUTION): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - unit_of_measurement=UNIT_MILLIMETER, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE, + unit_of_measurement=UNIT_MILLIMETER, ), } ) @@ -94,12 +178,42 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(f"zone_{n + 1}"): cv.Schema( { cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( + accuracy_decimals=0, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MAP_MARKER_ACCOUNT, ), cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema( + accuracy_decimals=0, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MAP_MARKER_ACCOUNT, ), cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema( + accuracy_decimals=0, + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MAP_MARKER_ACCOUNT, ), } diff --git a/esphome/components/ld2450/text_sensor.py b/esphome/components/ld2450/text_sensor.py index 6c11024b89..89b6939a29 100644 --- a/esphome/components/ld2450/text_sensor.py +++ b/esphome/components/ld2450/text_sensor.py @@ -56,7 +56,8 @@ async def to_code(config): sens = await text_sensor.new_text_sensor(mac_address_config) cg.add(ld2450_component.set_mac_text_sensor(sens)) for n in range(MAX_TARGETS): - if direction_conf := config.get(f"target_{n + 1}"): - if direction_config := direction_conf.get(CONF_DIRECTION): - sens = await text_sensor.new_text_sensor(direction_config) - cg.add(ld2450_component.set_direction_text_sensor(n, sens)) + if (direction_conf := config.get(f"target_{n + 1}")) and ( + direction_config := direction_conf.get(CONF_DIRECTION) + ): + sens = await text_sensor.new_text_sensor(direction_config) + cg.add(ld2450_component.set_direction_text_sensor(n, sens)) diff --git a/esphome/components/ld24xx/__init__.py b/esphome/components/ld24xx/__init__.py new file mode 100644 index 0000000000..516af84856 --- /dev/null +++ b/esphome/components/ld24xx/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@kbx81"] diff --git a/esphome/components/ld24xx/ld24xx.h b/esphome/components/ld24xx/ld24xx.h new file mode 100644 index 0000000000..1cd5e01163 --- /dev/null +++ b/esphome/components/ld24xx/ld24xx.h @@ -0,0 +1,65 @@ +#pragma once + +#include "esphome/core/defines.h" + +#include + +#ifdef USE_SENSOR +#include "esphome/core/helpers.h" +#include "esphome/components/sensor/sensor.h" + +#define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \ + protected: \ + ld24xx::SensorWithDedup *name##_sensor_{nullptr}; \ +\ + public: \ + void set_##name##_sensor(sensor::Sensor *sensor) { \ + this->name##_sensor_ = new ld24xx::SensorWithDedup(sensor); \ + } +#endif + +#define LOG_SENSOR_WITH_DEDUP_SAFE(tag, name, sensor) \ + if ((sensor) != nullptr) { \ + LOG_SENSOR(tag, name, (sensor)->sens); \ + } + +#define SAFE_PUBLISH_SENSOR(sensor, value) \ + if ((sensor) != nullptr) { \ + (sensor)->publish_state_if_not_dup(value); \ + } + +#define SAFE_PUBLISH_SENSOR_UNKNOWN(sensor) \ + if ((sensor) != nullptr) { \ + (sensor)->publish_state_unknown(); \ + } + +#define highbyte(val) (uint8_t)((val) >> 8) +#define lowbyte(val) (uint8_t)((val) &0xff) + +namespace esphome { +namespace ld24xx { + +#ifdef USE_SENSOR +// Helper class to store a sensor with a deduplicator & publish state only when the value changes +template class SensorWithDedup { + public: + SensorWithDedup(sensor::Sensor *sens) : sens(sens) {} + + void publish_state_if_not_dup(T state) { + if (this->publish_dedup.next(state)) { + this->sens->publish_state(static_cast(state)); + } + } + + void publish_state_unknown() { + if (this->publish_dedup.next_unknown()) { + this->sens->publish_state(NAN); + } + } + + sensor::Sensor *sens; + Deduplicator publish_dedup; +}; +#endif +} // namespace ld24xx +} // namespace esphome diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 2ae2656f54..aaa4794586 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -116,7 +116,6 @@ void LEDCOutput::write_state(float state) { } void LEDCOutput::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); auto speed_mode = get_speed_mode(this->channel_); auto timer_num = static_cast((this->channel_ % 8) / 2); auto chan_num = static_cast(this->channel_ % 8); diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 17d5d46ffd..178660cb40 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, + ThreadModel, __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(ThreadModel.MULTI_NO_ATOMICS) # force using arduino framework cg.add_platformio_option("framework", "arduino") diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 7ab899edb2..fa39721ee2 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -285,5 +285,4 @@ async def new_light(config, *args): @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_LIGHT") cg.add_global(light_ns.using) diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index 67c318eb8e..15d9272d1a 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -29,6 +29,7 @@ from esphome.const import ( CONF_WHITE, CONF_WIDTH, ) +from esphome.cpp_generator import MockObjClass from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.util import Registry @@ -88,8 +89,15 @@ ADDRESSABLE_EFFECTS = [] EFFECTS_REGISTRY = Registry() -def register_effect(name, effect_type, default_name, schema, *extra_validators): - schema = cv.Schema(schema).extend( +def register_effect( + name: str, + effect_type: MockObjClass, + default_name: str, + schema: cv.Schema | dict, + *extra_validators, +): + schema = schema if isinstance(schema, cv.Schema) else cv.Schema(schema) + schema = schema.extend( { cv.Optional(CONF_NAME, default=default_name): cv.string_strict, } @@ -98,7 +106,13 @@ def register_effect(name, effect_type, default_name, schema, *extra_validators): return EFFECTS_REGISTRY.register(name, effect_type, validator) -def register_binary_effect(name, effect_type, default_name, schema, *extra_validators): +def register_binary_effect( + name: str, + effect_type: MockObjClass, + default_name: str, + schema: cv.Schema | dict, + *extra_validators, +): # binary effect can be used for all lights BINARY_EFFECTS.append(name) MONOCHROMATIC_EFFECTS.append(name) @@ -109,7 +123,11 @@ def register_binary_effect(name, effect_type, default_name, schema, *extra_valid def register_monochromatic_effect( - name, effect_type, default_name, schema, *extra_validators + name: str, + effect_type: MockObjClass, + default_name: str, + schema: cv.Schema | dict, + *extra_validators, ): # monochromatic effect can be used for all lights expect binary MONOCHROMATIC_EFFECTS.append(name) @@ -119,7 +137,13 @@ def register_monochromatic_effect( return register_effect(name, effect_type, default_name, schema, *extra_validators) -def register_rgb_effect(name, effect_type, default_name, schema, *extra_validators): +def register_rgb_effect( + name: str, + effect_type: MockObjClass, + default_name: str, + schema: cv.Schema | dict, + *extra_validators, +): # RGB effect can be used for RGB and addressable lights RGB_EFFECTS.append(name) ADDRESSABLE_EFFECTS.append(name) @@ -128,7 +152,11 @@ def register_rgb_effect(name, effect_type, default_name, schema, *extra_validato def register_addressable_effect( - name, effect_type, default_name, schema, *extra_validators + name: str, + effect_type: MockObjClass, + default_name: str, + schema: cv.Schema | dict, + *extra_validators, ): # addressable effect can be used only in addressable ADDRESSABLE_EFFECTS.append(name) @@ -291,31 +319,30 @@ async def random_effect_to_code(config, effect_id): ) async def strobe_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - colors = [] - for color in config.get(CONF_COLORS, []): - colors.append( - cg.StructInitializer( - StrobeLightEffectColor, - ( - "color", - LightColorValues( - color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), - color[CONF_STATE], - color[CONF_BRIGHTNESS], - color[CONF_COLOR_BRIGHTNESS], - color[CONF_RED], - color[CONF_GREEN], - color[CONF_BLUE], - color[CONF_WHITE], - color.get(CONF_COLOR_TEMPERATURE, 0.0), - color[CONF_COLD_WHITE], - color[CONF_WARM_WHITE], - ), + colors = [ + cg.StructInitializer( + StrobeLightEffectColor, + ( + "color", + LightColorValues( + color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), + color[CONF_STATE], + color[CONF_BRIGHTNESS], + color[CONF_COLOR_BRIGHTNESS], + color[CONF_RED], + color[CONF_GREEN], + color[CONF_BLUE], + color[CONF_WHITE], + color.get(CONF_COLOR_TEMPERATURE, 0.0), + color[CONF_COLD_WHITE], + color[CONF_WARM_WHITE], ), - ("duration", color[CONF_DURATION]), - ("transition_length", color[CONF_TRANSITION_LENGTH]), - ) + ), + ("duration", color[CONF_DURATION]), + ("transition_length", color[CONF_TRANSITION_LENGTH]), ) + for color in config.get(CONF_COLORS, []) + ] cg.add(var.set_colors(colors)) return var @@ -354,10 +381,9 @@ async def addressable_lambda_effect_to_code(config, effect_id): (bool, "initial_run"), ] lambda_ = await cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void) - var = cg.new_Pvariable( + return cg.new_Pvariable( effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL] ) - return var @register_addressable_effect( @@ -404,20 +430,19 @@ async def addressable_color_wipe_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_add_led_interval(config[CONF_ADD_LED_INTERVAL])) cg.add(var.set_reverse(config[CONF_REVERSE])) - colors = [] - for color in config.get(CONF_COLORS, []): - colors.append( - cg.StructInitializer( - AddressableColorWipeEffectColor, - ("r", int(round(color[CONF_RED] * 255))), - ("g", int(round(color[CONF_GREEN] * 255))), - ("b", int(round(color[CONF_BLUE] * 255))), - ("w", int(round(color[CONF_WHITE] * 255))), - ("random", color[CONF_RANDOM]), - ("num_leds", color[CONF_NUM_LEDS]), - ("gradient", color[CONF_GRADIENT]), - ) + colors = [ + cg.StructInitializer( + AddressableColorWipeEffectColor, + ("r", int(round(color[CONF_RED] * 255))), + ("g", int(round(color[CONF_GREEN] * 255))), + ("b", int(round(color[CONF_BLUE] * 255))), + ("w", int(round(color[CONF_WHITE] * 255))), + ("random", color[CONF_RANDOM]), + ("num_leds", color[CONF_NUM_LEDS]), + ("gradient", color[CONF_GRADIENT]), ) + for color in config.get(CONF_COLORS, []) + ] cg.add(var.set_colors(colors)) return var @@ -526,7 +551,7 @@ def validate_effects(allowed_effects): errors = [] names = set() for i, x in enumerate(value): - key = next(it for it in x.keys()) + key = next(it for it in x) if key not in allowed_effects: errors.append( cv.Invalid( diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index a3ffe22591..60945531cf 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -9,6 +9,28 @@ namespace light { static const char *const TAG = "light"; +// Helper functions to reduce code size for logging +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN +static void log_validation_warning(const char *name, const char *param_name, float val, float min, float max) { + ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, param_name, val, min, max); +} + +static void log_feature_not_supported(const char *name, const char *feature) { + ESP_LOGW(TAG, "'%s': %s not supported", name, feature); +} + +static void log_color_mode_not_supported(const char *name, const char *feature) { + ESP_LOGW(TAG, "'%s': color mode does not support setting %s", name, feature); +} + +static void log_invalid_parameter(const char *name, const char *message) { ESP_LOGW(TAG, "'%s': %s", name, message); } +#else +#define log_validation_warning(name, param_name, val, min, max) +#define log_feature_not_supported(name, feature) +#define log_color_mode_not_supported(name, feature) +#define log_invalid_parameter(name, message) +#endif + // Macro to reduce repetitive setter code #define IMPLEMENT_LIGHT_CALL_SETTER(name, type, flag) \ LightCall &LightCall::set_##name(optional(name)) { \ @@ -44,11 +66,21 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { return LOG_STR(""); } +// Helper to log percentage values +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +static void log_percent(const char *name, const char *param, float value) { + ESP_LOGD(TAG, " %s: %.0f%%", param, value * 100.0f); +} +#else +#define log_percent(name, param, value) +#endif + void LightCall::perform() { const char *name = this->parent_->get_name().c_str(); LightColorValues v = this->validate_(); + const bool publish = this->get_publish_(); - if (this->get_publish_()) { + if (publish) { ESP_LOGD(TAG, "'%s' Setting:", name); // Only print color mode when it's being changed @@ -66,11 +98,11 @@ void LightCall::perform() { } if (this->has_brightness()) { - ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f); + log_percent(name, "Brightness", v.get_brightness()); } if (this->has_color_brightness()) { - ESP_LOGD(TAG, " Color brightness: %.0f%%", v.get_color_brightness() * 100.0f); + log_percent(name, "Color brightness", v.get_color_brightness()); } if (this->has_red() || this->has_green() || this->has_blue()) { ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, @@ -78,7 +110,7 @@ void LightCall::perform() { } if (this->has_white()) { - ESP_LOGD(TAG, " White: %.0f%%", v.get_white() * 100.0f); + log_percent(name, "White", v.get_white()); } if (this->has_color_temperature()) { ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); @@ -92,26 +124,26 @@ void LightCall::perform() { if (this->has_flash_()) { // FLASH - if (this->get_publish_()) { + if (publish) { ESP_LOGD(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f); } - this->parent_->start_flash_(v, this->flash_length_, this->get_publish_()); + this->parent_->start_flash_(v, this->flash_length_, publish); } else if (this->has_transition_()) { // TRANSITION - if (this->get_publish_()) { + if (publish) { ESP_LOGD(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f); } // Special case: Transition and effect can be set when turning off if (this->has_effect_()) { - if (this->get_publish_()) { + if (publish) { ESP_LOGD(TAG, " Effect: 'None'"); } this->parent_->stop_effect_(); } - this->parent_->start_transition_(v, this->transition_length_, this->get_publish_()); + this->parent_->start_transition_(v, this->transition_length_, publish); } else if (this->has_effect_()) { // EFFECT @@ -122,7 +154,7 @@ void LightCall::perform() { effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str(); } - if (this->get_publish_()) { + if (publish) { ESP_LOGD(TAG, " Effect: '%s'", effect_s); } @@ -133,13 +165,13 @@ void LightCall::perform() { this->parent_->set_immediately_(v, true); } else { // INSTANT CHANGE - this->parent_->set_immediately_(v, this->get_publish_()); + this->parent_->set_immediately_(v, publish); } if (!this->has_transition_()) { this->parent_->target_state_reached_callback_.call(); } - if (this->get_publish_()) { + if (publish) { this->parent_->publish_state(); } if (this->get_save_()) { @@ -169,19 +201,19 @@ LightColorValues LightCall::validate_() { // Brightness exists check if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { - ESP_LOGW(TAG, "'%s': setting brightness not supported", name); + log_feature_not_supported(name, "brightness"); this->set_flag_(FLAG_HAS_BRIGHTNESS, false); } // Transition length possible check if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) { - ESP_LOGW(TAG, "'%s': transitions not supported", name); + log_feature_not_supported(name, "transitions"); this->set_flag_(FLAG_HAS_TRANSITION, false); } // Color brightness exists check if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { - ESP_LOGW(TAG, "'%s': color mode does not support setting RGB brightness", name); + log_color_mode_not_supported(name, "RGB brightness"); this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false); } @@ -189,7 +221,7 @@ LightColorValues LightCall::validate_() { if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) || (this->has_blue() && this->blue_ > 0.0f)) { if (!(color_mode & ColorCapability::RGB)) { - ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name); + log_color_mode_not_supported(name, "RGB color"); this->set_flag_(FLAG_HAS_RED, false); this->set_flag_(FLAG_HAS_GREEN, false); this->set_flag_(FLAG_HAS_BLUE, false); @@ -199,21 +231,21 @@ LightColorValues LightCall::validate_() { // White value exists check if (this->has_white() && this->white_ > 0.0f && !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s': color mode does not support setting white value", name); + log_color_mode_not_supported(name, "white value"); this->set_flag_(FLAG_HAS_WHITE, false); } // Color temperature exists check if (this->has_color_temperature() && !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s': color mode does not support setting color temperature", name); + log_color_mode_not_supported(name, "color temperature"); this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false); } // Cold/warm white value exists check if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) { if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { - ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name); + log_color_mode_not_supported(name, "cold/warm white value"); this->set_flag_(FLAG_HAS_COLD_WHITE, false); this->set_flag_(FLAG_HAS_WARM_WHITE, false); } @@ -223,8 +255,7 @@ LightColorValues LightCall::validate_() { if (this->has_##name_()) { \ auto val = this->name_##_; \ if (val < (min) || val > (max)) { \ - ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_LITERAL(upper_name), val, \ - (min), (max)); \ + log_validation_warning(name, LOG_STR_LITERAL(upper_name), val, (min), (max)); \ this->name_##_ = clamp(val, (min), (max)); \ } \ } @@ -288,7 +319,7 @@ LightColorValues LightCall::validate_() { // Flash length check if (this->has_flash_() && this->flash_length_ == 0) { - ESP_LOGW(TAG, "'%s': flash length must be greater than zero", name); + log_invalid_parameter(name, "flash length must be greater than zero"); this->set_flag_(FLAG_HAS_FLASH, false); } @@ -307,13 +338,13 @@ LightColorValues LightCall::validate_() { } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { - ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name); + log_invalid_parameter(name, "effect cannot be used with transition/flash"); this->set_flag_(FLAG_HAS_TRANSITION, false); this->set_flag_(FLAG_HAS_FLASH, false); } if (this->has_flash_() && this->has_transition_()) { - ESP_LOGW(TAG, "'%s': flash cannot be used with transition", name); + log_invalid_parameter(name, "flash cannot be used with transition"); this->set_flag_(FLAG_HAS_TRANSITION, false); } @@ -330,7 +361,7 @@ LightColorValues LightCall::validate_() { } if (this->has_transition_() && !supports_transition) { - ESP_LOGW(TAG, "'%s': transitions not supported", name); + log_feature_not_supported(name, "transitions"); this->set_flag_(FLAG_HAS_TRANSITION, false); } @@ -340,7 +371,7 @@ LightColorValues LightCall::validate_() { bool target_state = this->has_state() ? this->state_ : v.is_on(); if (!this->has_flash_() && !target_state) { if (this->has_effect_()) { - ESP_LOGW(TAG, "'%s': cannot start effect when turning off", name); + log_invalid_parameter(name, "cannot start effect when turning off"); this->set_flag_(FLAG_HAS_EFFECT, false); } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect @@ -364,21 +395,27 @@ void LightCall::transform_parameters_() { // - RGBWW lights with color_interlock=true, which also sets "brightness" and // "color_temperature" (without color_interlock, CW/WW are set directly) // - Legacy Home Assistant (pre-colormode), which sets "white" and "color_temperature" + + // Cache min/max mireds to avoid repeated calls + const float min_mireds = traits.get_min_mireds(); + const float max_mireds = traits.get_max_mireds(); + if (((this->has_white() && this->white_ > 0.0f) || this->has_color_temperature()) && // (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && // !(this->color_mode_ & ColorCapability::WHITE) && // !(this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // - traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) { + min_mireds > 0.0f && max_mireds > 0.0f) { ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values", this->parent_->get_name().c_str()); if (this->has_color_temperature()) { - const float color_temp = clamp(this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); - const float ww_fraction = - (color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds()); + const float color_temp = clamp(this->color_temperature_, min_mireds, max_mireds); + const float range = max_mireds - min_mireds; + const float ww_fraction = (color_temp - min_mireds) / range; const float cw_fraction = 1.0f - ww_fraction; const float max_cw_ww = std::max(ww_fraction, cw_fraction); - this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct()); - this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct()); + const float gamma = this->parent_->get_gamma_correct(); + this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma); + this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma); this->set_flag_(FLAG_HAS_COLD_WHITE, true); this->set_flag_(FLAG_HAS_WARM_WHITE, true); } @@ -442,41 +479,39 @@ std::set LightCall::get_suitable_color_modes_() { bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) || (this->has_red() || this->has_green() || this->has_blue()); +// Build key from flags: [rgb][cwww][ct][white] #define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) -#define ENTRY(white, ct, cwww, rgb, ...) \ - std::make_tuple>(KEY(white, ct, cwww, rgb), __VA_ARGS__) - // Flag order: white, color temperature, cwww, rgb - std::array>, 10> lookup_table{ - ENTRY(true, false, false, false, - {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, - ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(false, true, false, false, - {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, - ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(true, true, false, false, - {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(false, false, true, false, {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(false, false, false, false, - {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, - ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}), - ENTRY(true, false, false, true, - {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(false, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(true, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(false, false, true, true, {ColorMode::RGB_COLD_WARM_WHITE}), - ENTRY(false, false, false, true, - {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), - }; + uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb); - auto key = KEY(has_white, has_ct, has_cwww, has_rgb); - for (auto &item : lookup_table) { - if (std::get<0>(item) == key) - return std::get<1>(item); + switch (key) { + case KEY(true, false, false, false): // white only + return {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(false, true, false, false): // ct only + return {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(true, true, false, false): // white + ct + return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(false, false, true, false): // cwww only + return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(false, false, false, false): // none + return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, + ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}; + case KEY(true, false, false, true): // rgb + white + return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(false, true, false, true): // rgb + ct + case KEY(true, true, false, true): // rgb + white + ct + return {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(false, false, true, true): // rgb + cwww + return {ColorMode::RGB_COLD_WARM_WHITE}; + case KEY(false, false, false, true): // rgb only + return {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}; + default: + return {}; // conflicting flags } - // This happens if there are conflicting flags given. - return {}; +#undef KEY } LightCall &LightCall::set_effect(const std::string &effect) { diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 5653a8d2a5..04d7d1e7d8 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -84,18 +84,23 @@ class LightColorValues { * @return The linearly interpolated LightColorValues. */ static LightColorValues lerp(const LightColorValues &start, const LightColorValues &end, float completion) { + // Directly interpolate the raw values to avoid getter/setter overhead. + // This is safe because: + // - All LightColorValues have their values clamped when set via the setters + // - std::lerp guarantees output is in the same range as inputs + // - Therefore the output doesn't need clamping, so we can skip the setters LightColorValues v; - v.set_color_mode(end.color_mode_); - v.set_state(std::lerp(start.get_state(), end.get_state(), completion)); - v.set_brightness(std::lerp(start.get_brightness(), end.get_brightness(), completion)); - v.set_color_brightness(std::lerp(start.get_color_brightness(), end.get_color_brightness(), completion)); - v.set_red(std::lerp(start.get_red(), end.get_red(), completion)); - v.set_green(std::lerp(start.get_green(), end.get_green(), completion)); - v.set_blue(std::lerp(start.get_blue(), end.get_blue(), completion)); - v.set_white(std::lerp(start.get_white(), end.get_white(), completion)); - v.set_color_temperature(std::lerp(start.get_color_temperature(), end.get_color_temperature(), completion)); - v.set_cold_white(std::lerp(start.get_cold_white(), end.get_cold_white(), completion)); - v.set_warm_white(std::lerp(start.get_warm_white(), end.get_warm_white(), completion)); + v.color_mode_ = end.color_mode_; + v.state_ = std::lerp(start.state_, end.state_, completion); + v.brightness_ = std::lerp(start.brightness_, end.brightness_, completion); + v.color_brightness_ = std::lerp(start.color_brightness_, end.color_brightness_, completion); + v.red_ = std::lerp(start.red_, end.red_, completion); + v.green_ = std::lerp(start.green_, end.green_, completion); + v.blue_ = std::lerp(start.blue_, end.blue_, completion); + v.white_ = std::lerp(start.white_, end.white_, completion); + v.color_temperature_ = std::lerp(start.color_temperature_, end.color_temperature_, completion); + v.cold_white_ = std::lerp(start.cold_white_, end.cold_white_, completion); + v.warm_white_ = std::lerp(start.warm_white_, end.warm_white_, completion); return v; } diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 26615bae5c..896b821705 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -8,6 +8,32 @@ namespace light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema +// Lookup table for color mode strings +static constexpr const char *get_color_mode_json_str(ColorMode mode) { + switch (mode) { + case ColorMode::ON_OFF: + return "onoff"; + case ColorMode::BRIGHTNESS: + return "brightness"; + case ColorMode::WHITE: + return "white"; // not supported by HA in MQTT + case ColorMode::COLOR_TEMPERATURE: + return "color_temp"; + case ColorMode::COLD_WARM_WHITE: + return "cwww"; // not supported by HA + case ColorMode::RGB: + return "rgb"; + case ColorMode::RGB_WHITE: + return "rgbw"; + case ColorMode::RGB_COLOR_TEMPERATURE: + return "rgbct"; // not supported by HA + case ColorMode::RGB_COLD_WARM_WHITE: + return "rgbww"; + default: + return nullptr; + } +} + void LightJSONSchema::dump_json(LightState &state, JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson if (state.supports_effects()) @@ -16,60 +42,36 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) { auto values = state.remote_values; auto traits = state.get_output()->get_traits(); - switch (values.get_color_mode()) { - case ColorMode::UNKNOWN: // don't need to set color mode if we don't know it - break; - case ColorMode::ON_OFF: - root["color_mode"] = "onoff"; - break; - case ColorMode::BRIGHTNESS: - root["color_mode"] = "brightness"; - break; - case ColorMode::WHITE: // not supported by HA in MQTT - root["color_mode"] = "white"; - break; - case ColorMode::COLOR_TEMPERATURE: - root["color_mode"] = "color_temp"; - break; - case ColorMode::COLD_WARM_WHITE: // not supported by HA - root["color_mode"] = "cwww"; - break; - case ColorMode::RGB: - root["color_mode"] = "rgb"; - break; - case ColorMode::RGB_WHITE: - root["color_mode"] = "rgbw"; - break; - case ColorMode::RGB_COLOR_TEMPERATURE: // not supported by HA - root["color_mode"] = "rgbct"; - break; - case ColorMode::RGB_COLD_WARM_WHITE: - root["color_mode"] = "rgbww"; - break; + const auto color_mode = values.get_color_mode(); + const char *mode_str = get_color_mode_json_str(color_mode); + if (mode_str != nullptr) { + root["color_mode"] = mode_str; } - if (values.get_color_mode() & ColorCapability::ON_OFF) + if (color_mode & ColorCapability::ON_OFF) root["state"] = (values.get_state() != 0.0f) ? "ON" : "OFF"; - if (values.get_color_mode() & ColorCapability::BRIGHTNESS) - root["brightness"] = uint8_t(values.get_brightness() * 255); + if (color_mode & ColorCapability::BRIGHTNESS) + root["brightness"] = to_uint8_scale(values.get_brightness()); 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); - color["b"] = uint8_t(values.get_color_brightness() * values.get_blue() * 255); + if (color_mode & ColorCapability::RGB) { + float color_brightness = values.get_color_brightness(); + color["r"] = to_uint8_scale(color_brightness * values.get_red()); + color["g"] = to_uint8_scale(color_brightness * values.get_green()); + color["b"] = to_uint8_scale(color_brightness * values.get_blue()); } - if (values.get_color_mode() & ColorCapability::WHITE) { - color["w"] = uint8_t(values.get_white() * 255); - root["white_value"] = uint8_t(values.get_white() * 255); // legacy API + if (color_mode & ColorCapability::WHITE) { + uint8_t white_val = to_uint8_scale(values.get_white()); + color["w"] = white_val; + root["white_value"] = white_val; // legacy API } - if (values.get_color_mode() & ColorCapability::COLOR_TEMPERATURE) { + if (color_mode & ColorCapability::COLOR_TEMPERATURE) { // this one isn't under the color subkey for some reason root["color_temp"] = uint32_t(values.get_color_temperature()); } - if (values.get_color_mode() & ColorCapability::COLD_WARM_WHITE) { - color["c"] = uint8_t(values.get_cold_white() * 255); - color["w"] = uint8_t(values.get_warm_white() * 255); + if (color_mode & ColorCapability::COLD_WARM_WHITE) { + color["c"] = to_uint8_scale(values.get_cold_white()); + color["w"] = to_uint8_scale(values.get_warm_white()); } } diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 0aae6aed15..5b57707d6b 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -18,15 +18,14 @@ LightCall LightState::toggle() { return this->make_call().set_state(!this->remot LightCall LightState::make_call() { return LightCall(this); } void LightState::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str()); - this->output_->setup_state(this); for (auto *effect : this->effects_) { effect->init_internal(this); } // When supported color temperature range is known, initialize color temperature setting within bounds. - float min_mireds = this->get_traits().get_min_mireds(); + auto traits = this->get_traits(); + float min_mireds = traits.get_min_mireds(); if (min_mireds > 0) { this->remote_values.set_color_temperature(min_mireds); this->current_values.set_color_temperature(min_mireds); @@ -45,11 +44,8 @@ void LightState::setup() { this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); // Attempt to load from preferences, else fall back to default values if (!this->rtc_.load(&recovered)) { - recovered.state = false; - if (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON || - this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_ON) { - recovered.state = true; - } + recovered.state = (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON || + this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_ON); } else if (this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_OFF || this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_ON) { // Inverted restore state @@ -90,17 +86,18 @@ void LightState::setup() { } void LightState::dump_config() { ESP_LOGCONFIG(TAG, "Light '%s'", this->get_name().c_str()); - if (this->get_traits().supports_color_capability(ColorCapability::BRIGHTNESS)) { + auto traits = this->get_traits(); + if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) { ESP_LOGCONFIG(TAG, " Default Transition Length: %.1fs\n" " Gamma Correct: %.2f", this->default_transition_length_ / 1e3f, this->gamma_correct_); } - if (this->get_traits().supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) { + if (traits.supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) { ESP_LOGCONFIG(TAG, " Min Mireds: %.1f\n" " Max Mireds: %.1f", - this->get_traits().get_min_mireds(), this->get_traits().get_max_mireds()); + traits.get_min_mireds(), traits.get_max_mireds()); } } void LightState::loop() { diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 7c99d721f0..a45301d148 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -5,6 +5,13 @@ #include namespace esphome { + +#ifdef USE_API +namespace api { +class APIConnection; +} // namespace api +#endif + namespace light { /// This class is used to represent the capabilities of a light. @@ -52,6 +59,16 @@ class LightTraits { void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; } protected: +#ifdef USE_API + // The API connection is a friend class to access internal methods + friend class api::APIConnection; + // This method returns a reference to the internal color modes set. + // It is used by the API to avoid copying data when encoding messages. + // Warning: Do not use this method outside of the API connection code. + // It returns a reference to internal data that can be invalidated. + const std::set &get_supported_color_modes_for_api_() const { return this->supported_color_modes_; } +#endif + std::set supported_color_modes_{}; float min_mireds_{0}; float max_mireds_{0}; diff --git a/esphome/components/lightwaverf/lightwaverf.cpp b/esphome/components/lightwaverf/lightwaverf.cpp index 626e5747b7..31ac1fc576 100644 --- a/esphome/components/lightwaverf/lightwaverf.cpp +++ b/esphome/components/lightwaverf/lightwaverf.cpp @@ -14,8 +14,6 @@ static const bool DEFAULT_INVERT = false; static const uint32_t DEFAULT_TICK = 330; void LightWaveRF::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->lwtx_.lwtx_setup(pin_tx_, DEFAULT_REPEAT, DEFAULT_INVERT, DEFAULT_TICK); this->lwrx_.lwrx_setup(pin_rx_); } diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index c472a9f669..b29e4c2154 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -24,7 +24,6 @@ static const uint8_t READ_TOUCH[1] = {0x07}; } void LilygoT547Touchscreen::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); this->interrupt_pin_->setup(); diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index e62d9f3e2b..7977efd264 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -158,4 +158,3 @@ async def lock_is_off_to_code(config, condition_id, template_arg, args): @coroutine_with_priority(100.0) async def to_code(config): cg.add_global(lock_ns.using) - cg.add_define("USE_LOCK") diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 618f5bcc7a..d8c95d75f2 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 @@ -186,6 +196,7 @@ LoggerMessageTrigger = logger_ns.class_( 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, @@ -333,14 +346,13 @@ async def to_code(config): if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") - if CORE.using_arduino: - if config[CONF_HARDWARE_UART] == USB_CDC: - cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") - if CORE.is_esp32 and get_esp32_variant() in ( - VARIANT_ESP32C3, - VARIANT_ESP32C6, - ): - cg.add_build_flag("-DARDUINO_USB_MODE=1") + if CORE.using_arduino and config[CONF_HARDWARE_UART] == USB_CDC: + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") + if CORE.is_esp32 and get_esp32_variant() in ( + VARIANT_ESP32C3, + VARIANT_ESP32C6, + ): + cg.add_build_flag("-DARDUINO_USB_MODE=1") if CORE.using_esp_idf: if config[CONF_HARDWARE_UART] == USB_CDC: @@ -358,6 +370,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) @@ -388,7 +409,7 @@ def validate_printf(value): [cCdiouxXeEfgGaAnpsSZ] # type ) """ # noqa - matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.X) + matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE) if len(matches) != len(value[CONF_ARGS]): raise cv.Invalid( f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!" @@ -463,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..195e04948d 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -4,12 +4,11 @@ #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 { +namespace esphome::logger { static const char *const TAG = "logger"; @@ -160,6 +159,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 +173,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 +187,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 +234,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 @@ -276,5 +282,4 @@ void Logger::set_log_level(uint8_t level) { Logger *global_logger = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace logger -} // namespace esphome +} // namespace esphome::logger diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index fb68e75a51..aa76a188c9 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -29,9 +29,12 @@ #include #endif // USE_ESP_IDF -namespace esphome { +#ifdef USE_ZEPHYR +#include +struct device; +#endif -namespace logger { +namespace esphome::logger { // Color and letter constants for log levels static const char *const LOG_LEVEL_COLORS[] = { @@ -56,7 +59,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 +85,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 +110,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 +125,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 +160,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 +168,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 +235,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 +263,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 +275,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 +289,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 +332,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, @@ -396,6 +409,4 @@ class LoggerMessageTrigger : public Trigger uint8_t level_; }; -} // namespace logger - -} // namespace esphome +} // namespace esphome::logger diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 2fde0f7d49..44243d4aa8 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -29,8 +29,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace logger { +namespace esphome::logger { static const char *const TAG = "logger"; @@ -119,9 +118,6 @@ void Logger::pre_setup() { #ifdef USE_LOGGER_USB_CDC case UART_SELECTION_USB_CDC: this->hw_serial_ = &Serial; -#if ARDUINO_USB_CDC_ON_BOOT - Serial.setTxTimeoutMs(0); // workaround for 2.0.9 crash when there's no data connection -#endif Serial.begin(this->baud_rate_); break; #endif @@ -209,6 +205,5 @@ const char *const UART_SELECTIONS[] = { const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } -} // namespace logger -} // namespace esphome +} // namespace esphome::logger #endif diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index 5bfeb21007..fb5f6cee5d 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -2,8 +2,7 @@ #include "logger.h" #include "esphome/core/log.h" -namespace esphome { -namespace logger { +namespace esphome::logger { static const char *const TAG = "logger"; @@ -40,6 +39,5 @@ const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } -} // namespace logger -} // namespace esphome +} // namespace esphome::logger #endif diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index edffb1a8c3..4abe92286a 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -1,8 +1,7 @@ #if defined(USE_HOST) #include "logger.h" -namespace esphome { -namespace logger { +namespace esphome::logger { void HOT Logger::write_msg_(const char *msg) { time_t rawtime; @@ -18,7 +17,6 @@ void HOT Logger::write_msg_(const char *msg) { void Logger::pre_setup() { global_logger = this; } -} // namespace logger -} // namespace esphome +} // namespace esphome::logger #endif diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index 12e55b7cef..09d0622bc3 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -1,8 +1,7 @@ #ifdef USE_LIBRETINY #include "logger.h" -namespace esphome { -namespace logger { +namespace esphome::logger { static const char *const TAG = "logger"; @@ -56,7 +55,6 @@ const char *const UART_SELECTIONS[] = {"DEFAULT", "UART0", "UART1", "UART2"}; const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } -} // namespace logger -} // namespace esphome +} // namespace esphome::logger #endif // USE_LIBRETINY diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index 2783d77ac1..f1cad9b283 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -2,8 +2,7 @@ #include "logger.h" #include "esphome/core/log.h" -namespace esphome { -namespace logger { +namespace esphome::logger { static const char *const TAG = "logger"; @@ -34,6 +33,5 @@ const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } -} // namespace logger -} // namespace esphome +} // namespace esphome::logger #endif // USE_RP2040 diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp new file mode 100644 index 0000000000..58a09facd5 --- /dev/null +++ b/esphome/components/logger/logger_zephyr.cpp @@ -0,0 +1,86 @@ +#ifdef USE_ZEPHYR + +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "logger.h" + +#include +#include +#include + +namespace esphome::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 esphome::logger + +#endif diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index b71a6e02a2..d9c950ce3c 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -1,7 +1,6 @@ #include "logger_level_select.h" -namespace esphome { -namespace logger { +namespace esphome::logger { void LoggerLevelSelect::publish_state(int level) { auto value = this->at(level); @@ -23,5 +22,4 @@ void LoggerLevelSelect::control(const std::string &value) { this->parent_->set_log_level(level.value()); } -} // namespace logger -} // namespace esphome +} // namespace esphome::logger diff --git a/esphome/components/logger/select/logger_level_select.h b/esphome/components/logger/select/logger_level_select.h index 2c92c84d13..f31a6f6cdb 100644 --- a/esphome/components/logger/select/logger_level_select.h +++ b/esphome/components/logger/select/logger_level_select.h @@ -3,13 +3,11 @@ #include "esphome/components/select/select.h" #include "esphome/core/component.h" #include "esphome/components/logger/logger.h" -namespace esphome { -namespace logger { +namespace esphome::logger { class LoggerLevelSelect : public Component, public select::Select, public Parented { public: void publish_state(int level); void setup() override; void control(const std::string &value) override; }; -} // namespace logger -} // namespace esphome +} // namespace esphome::logger diff --git a/esphome/components/logger/task_log_buffer.cpp b/esphome/components/logger/task_log_buffer.cpp index 24d9284f1a..b5dd9f0239 100644 --- a/esphome/components/logger/task_log_buffer.cpp +++ b/esphome/components/logger/task_log_buffer.cpp @@ -5,8 +5,7 @@ #ifdef USE_ESPHOME_TASK_LOG_BUFFER -namespace esphome { -namespace logger { +namespace esphome::logger { TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { // Store the buffer size @@ -132,7 +131,6 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin return true; } -} // namespace logger -} // namespace esphome +} // namespace esphome::logger #endif // USE_ESPHOME_TASK_LOG_BUFFER diff --git a/esphome/components/logger/task_log_buffer.h b/esphome/components/logger/task_log_buffer.h index 1618a5a121..fdda07190d 100644 --- a/esphome/components/logger/task_log_buffer.h +++ b/esphome/components/logger/task_log_buffer.h @@ -11,8 +11,7 @@ #include #include -namespace esphome { -namespace logger { +namespace esphome::logger { class TaskLogBuffer { public: @@ -63,7 +62,6 @@ class TaskLogBuffer { mutable uint16_t last_processed_counter_{0}; // Tracks last processed message }; -} // namespace logger -} // namespace esphome +} // namespace esphome::logger #endif // USE_ESPHOME_TASK_LOG_BUFFER diff --git a/esphome/components/lps22/sensor.py b/esphome/components/lps22/sensor.py index 87a2106308..08e97ee7b7 100644 --- a/esphome/components/lps22/sensor.py +++ b/esphome/components/lps22/sensor.py @@ -1,16 +1,16 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import i2c, sensor +import esphome.config_validation as cv from esphome.const import ( CONF_ID, - CONF_TEMPERATURE, CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + ICON_THERMOMETER, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_HECTOPASCAL, - ICON_THERMOMETER, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_PRESSURE, ) CODEOWNERS = ["@nagisa"] diff --git a/esphome/components/ltr390/ltr390.cpp b/esphome/components/ltr390/ltr390.cpp index cc7e686d13..c1885dcb6f 100644 --- a/esphome/components/ltr390/ltr390.cpp +++ b/esphome/components/ltr390/ltr390.cpp @@ -148,8 +148,6 @@ void LTR390Component::read_mode_(int mode_index) { } void LTR390Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // reset std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); ctrl[LTR390_CTRL_RST] = true; diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index 12f227ab91..b249d23666 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -74,7 +74,6 @@ static float get_ps_gain_coeff(PsGain501 gain) { } void LTRAlsPs501Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // As per datasheet we need to wait at least 100ms after power on to get ALS chip responsive this->set_timeout(100, [this]() { this->state_ = State::DELAYED_SETUP; }); } diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp index 9b635a12b1..bf27c01e26 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.cpp +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -63,7 +63,6 @@ static float get_ps_gain_coeff(PsGain gain) { } void LTRAlsPsComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // As per datasheet we need to wait at least 100ms after power on to get ALS chip responsive this->set_timeout(100, [this]() { this->state_ = State::DELAYED_SETUP; }); } diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 4a450375c4..5af61300da 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -2,8 +2,9 @@ import logging from esphome.automation import build_automation, register_action, validate_automation import esphome.codegen as cg -from esphome.components.const import CONF_DRAW_ROUNDING +from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING from esphome.components.display import Display +from esphome.components.psram import DOMAIN as PSRAM_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_AUTO_CLEAR_ENABLED, @@ -11,6 +12,7 @@ from esphome.const import ( CONF_GROUP, CONF_ID, CONF_LAMBDA, + CONF_LOG_LEVEL, CONF_ON_BOOT, CONF_ON_IDLE, CONF_PAGES, @@ -185,8 +187,8 @@ def multi_conf_validate(configs: list[dict]): base_config = configs[0] for config in configs[1:]: for item in ( - df.CONF_LOG_LEVEL, - df.CONF_COLOR_DEPTH, + CONF_LOG_LEVEL, + CONF_COLOR_DEPTH, df.CONF_BYTE_ORDER, df.CONF_TRANSPARENCY_KEY, ): @@ -201,9 +203,8 @@ def final_validation(configs): multi_conf_validate(configs) global_config = full_config.get() for config in configs: - if pages := config.get(CONF_PAGES): - if all(p[df.CONF_SKIP] for p in pages): - raise cv.Invalid("At least one page must not be skipped") + if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages): + raise cv.Invalid("At least one page must not be skipped") for display_id in config[df.CONF_DISPLAYS]: path = global_config.get_path_for_id(display_id)[:-1] display = global_config.get_config_for_path(path) @@ -220,7 +221,7 @@ def final_validation(configs): draw_rounding, config[CONF_DRAW_ROUNDING] ) buffer_frac = config[CONF_BUFFER_SIZE] - if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: + if CORE.is_esp32 and buffer_frac > 0.5 and PSRAM_DOMAIN not in global_config: LOGGER.warning("buffer_size: may need to be reduced without PSRAM") for image_id in lv_images_used: path = global_config.get_path_for_id(image_id)[:-1] @@ -269,17 +270,17 @@ async def to_code(configs): add_define( "LV_LOG_LEVEL", - f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[df.CONF_LOG_LEVEL]]}", + f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[CONF_LOG_LEVEL]]}", ) cg.add_define( "LVGL_LOG_LEVEL", - cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[df.CONF_LOG_LEVEL]}"), + cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[CONF_LOG_LEVEL]}"), ) - add_define("LV_COLOR_DEPTH", config_0[df.CONF_COLOR_DEPTH]) + add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH]) for font in helpers.lv_fonts_used: add_define(f"LV_FONT_{font.upper()}") - if config_0[df.CONF_COLOR_DEPTH] == 16: + if config_0[CONF_COLOR_DEPTH] == 16: add_define( "LV_COLOR_16_SWAP", "1" if config_0[df.CONF_BYTE_ORDER] == "big_endian" else "0", @@ -416,14 +417,14 @@ LVGL_SCHEMA = cv.All( { cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent), cv.GenerateID(df.CONF_DISPLAYS): display_schema, - cv.Optional(df.CONF_COLOR_DEPTH, default=16): cv.one_of(16), + cv.Optional(CONF_COLOR_DEPTH, default=16): cv.one_of(16), cv.Optional( df.CONF_DEFAULT_FONT, default="montserrat_14" ): lvalid.lv_font, cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean, cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int, cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage, - cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of( + cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of( *df.LV_LOG_LEVELS, upper=True ), cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index cc0f833ced..fc70b0f682 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -85,8 +85,7 @@ async def action_to_code( async with LambdaContext(parameters=args, where=action_id) as context: for widget in widgets: await action(widget) - var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) - return var + return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) async def update_to_code(config, action_id, template_arg, args): @@ -354,8 +353,7 @@ async def widget_focus(config, action_id, template_arg, args): if config[CONF_FREEZE]: lv.group_focus_freeze(group, True) - var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) - return var + return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) @automation.register_action( diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index baa9a19c51..8f09a3a6d0 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -418,7 +418,6 @@ CONF_BUTTONS = "buttons" CONF_BYTE_ORDER = "byte_order" CONF_CHANGE_RATE = "change_rate" CONF_CLOSE_BUTTON = "close_button" -CONF_COLOR_DEPTH = "color_depth" CONF_CONTROL = "control" CONF_DEFAULT_FONT = "default_font" CONF_DEFAULT_GROUP = "default_group" @@ -457,7 +456,6 @@ CONF_KEYPADS = "keypads" CONF_LAYOUT = "layout" CONF_LEFT_BUTTON = "left_button" CONF_LINE_WIDTH = "line_width" -CONF_LOG_LEVEL = "log_level" CONF_LONG_PRESS_TIME = "long_press_time" CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" CONF_LVGL_ID = "lvgl_id" diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index e04a0105d5..8d5b6354bb 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -33,7 +33,7 @@ def validate_printf(value): [cCdiouxXeEfgGaAnpsSZ] # type ) """ # noqa - matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.X) + matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE) if len(matches) != len(value[CONF_ARGS]): raise cv.Invalid( f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!" diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 92fe74eb52..d345ac70f3 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -271,8 +271,7 @@ padding = LValidator(padding_validator, int32, retmapper=literal) def zoom_validator(value): - value = cv.float_range(0.1, 10.0)(value) - return value + return cv.float_range(0.1, 10.0)(value) def zoom_retmapper(value): @@ -288,10 +287,14 @@ def angle(value): :param value: The input in the range 0..360 :return: An angle in 1/10 degree units. """ - return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10) + return cv.float_range(0.0, 360.0)(cv.angle(value)) -lv_angle = LValidator(angle, uint32) +# Validator for angles in LVGL expressed in 1/10 degree units. +lv_angle = LValidator(angle, uint32, retmapper=lambda x: int(x * 10)) + +# Validator for angles in LVGL expressed in whole degrees +lv_angle_degrees = LValidator(angle, uint32, retmapper=int) @schema_extractor("one_of") diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index dd877df0f0..7a32691b53 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -434,7 +434,6 @@ LvglComponent::LvglComponent(std::vector displays, float buf } void LvglComponent::setup() { - ESP_LOGCONFIG(TAG, "LVGL Setup starts"); auto *display = this->displays_[0]; auto width = display->get_width(); auto height = display->get_height(); @@ -452,7 +451,8 @@ void LvglComponent::setup() { if (buffer == nullptr && this->buffer_frac_ == 0) { frac = MIN_BUFFER_FRAC; buffer_pixels /= MIN_BUFFER_FRAC; - buffer = lv_custom_mem_alloc(buf_bytes / MIN_BUFFER_FRAC); // NOLINT + buf_bytes /= MIN_BUFFER_FRAC; + buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT } if (buffer == nullptr) { this->status_set_error("Memory allocation failure"); @@ -489,7 +489,6 @@ void LvglComponent::setup() { disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0); lv_disp_trig_activity(this->disp_); - ESP_LOGCONFIG(TAG, "LVGL Setup complete"); } void LvglComponent::update() { diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 426dd3f229..3969c9f388 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -66,8 +66,7 @@ async def style_update_to_code(config, action_id, template_arg, args): async with LambdaContext(parameters=args, where=action_id) as context: await style_set(style, config) - var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) - return var + return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) async def theme_to_code(config): @@ -76,6 +75,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 10b6f63528..c19c89401a 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -161,7 +161,7 @@ class WidgetType: """ return [] - def obj_creator(self, parent: MockObjClass, config: dict): + async def obj_creator(self, parent: MockObjClass, config: dict): """ Create an instance of the widget type :param parent: The parent to which it should be attached diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index a8cb8dce33..bb6155234c 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -189,7 +189,7 @@ class Widget: for matrix buttons :return: """ - return None + return def get_max(self): return self.type.get_max(self.config) @@ -439,7 +439,7 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): :return: """ spec: WidgetType = WIDGET_TYPES[w_type] - creator = spec.obj_creator(parent, w_cnfig) + creator = await spec.obj_creator(parent, w_cnfig) add_lv_use(spec.name) add_lv_use(*spec.get_uses()) wid = w_cnfig[CONF_ID] diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py index 65f0e785b6..ef4da0d815 100644 --- a/esphome/components/lvgl/widgets/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -20,7 +20,7 @@ from ..defines import ( CONF_START_ANGLE, literal, ) -from ..lv_validation import angle, get_start_value, lv_float +from ..lv_validation import get_start_value, lv_angle_degrees, lv_float, lv_int from ..lvcode import lv, lv_expr, lv_obj from ..types import LvNumber, NumberType from . import Widget @@ -29,11 +29,11 @@ CONF_ARC = "arc" ARC_SCHEMA = cv.Schema( { cv.Optional(CONF_VALUE): lv_float, - cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, - cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, - cv.Optional(CONF_START_ANGLE, default=135): angle, - cv.Optional(CONF_END_ANGLE, default=45): angle, - cv.Optional(CONF_ROTATION, default=0.0): angle, + cv.Optional(CONF_MIN_VALUE, default=0): lv_int, + cv.Optional(CONF_MAX_VALUE, default=100): lv_int, + cv.Optional(CONF_START_ANGLE, default=135): lv_angle_degrees, + cv.Optional(CONF_END_ANGLE, default=45): lv_angle_degrees, + cv.Optional(CONF_ROTATION, default=0.0): lv_angle_degrees, cv.Optional(CONF_ADJUSTABLE, default=False): bool, cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of, cv.Optional(CONF_CHANGE_RATE, default=720): cv.uint16_t, @@ -59,11 +59,14 @@ class ArcType(NumberType): async def to_code(self, w: Widget, config): if CONF_MIN_VALUE in config: - lv.arc_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) - lv.arc_set_bg_angles( - w.obj, config[CONF_START_ANGLE] // 10, config[CONF_END_ANGLE] // 10 - ) - lv.arc_set_rotation(w.obj, config[CONF_ROTATION] // 10) + max_value = await lv_int.process(config[CONF_MAX_VALUE]) + min_value = await lv_int.process(config[CONF_MIN_VALUE]) + lv.arc_set_range(w.obj, min_value, max_value) + start = await lv_angle_degrees.process(config[CONF_START_ANGLE]) + end = await lv_angle_degrees.process(config[CONF_END_ANGLE]) + rotation = await lv_angle_degrees.process(config[CONF_ROTATION]) + lv.arc_set_bg_angles(w.obj, start, end) + lv.arc_set_rotation(w.obj, rotation) lv.arc_set_mode(w.obj, literal(config[CONF_MODE])) lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE]) diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index aa33be722c..c6b6d2440f 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -193,7 +193,7 @@ class ButtonMatrixType(WidgetType): async def to_code(self, w: Widget, config): lvgl_components_required.add("BUTTONMATRIX") if CONF_ROWS not in config: - return [] + return text_list, ctrl_list, width_list, key_list = await get_button_data( config[CONF_ROWS], w ) diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 7d8d13d8c4..028a81b449 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -4,7 +4,7 @@ from esphome.const import CONF_SIZE, CONF_TEXT from esphome.cpp_generator import MockObjClass from ..defines import CONF_MAIN -from ..lv_validation import color, color_retmapper, lv_text +from ..lv_validation import lv_color, lv_text from ..lvcode import LocalVariable, lv, lv_expr from ..schemas import TEXT_SCHEMA from ..types import WidgetType, lv_obj_t @@ -16,8 +16,8 @@ CONF_LIGHT_COLOR = "light_color" QRCODE_SCHEMA = TEXT_SCHEMA.extend( { - cv.Optional(CONF_DARK_COLOR, default="black"): color, - cv.Optional(CONF_LIGHT_COLOR, default="white"): color, + cv.Optional(CONF_DARK_COLOR, default="black"): lv_color, + cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color, cv.Required(CONF_SIZE): cv.int_, } ) @@ -34,11 +34,11 @@ class QrCodeType(WidgetType): ) def get_uses(self): - return ("canvas", "img", "label") + return "canvas", "img", "label" - def obj_creator(self, parent: MockObjClass, config: dict): - dark_color = color_retmapper(config[CONF_DARK_COLOR]) - light_color = color_retmapper(config[CONF_LIGHT_COLOR]) + async def obj_creator(self, parent: MockObjClass, config: dict): + dark_color = await lv_color.process(config[CONF_DARK_COLOR]) + light_color = await lv_color.process(config[CONF_LIGHT_COLOR]) size = config[CONF_SIZE] return lv_expr.call("qrcode_create", parent, size, dark_color, light_color) diff --git a/esphome/components/lvgl/widgets/spinner.py b/esphome/components/lvgl/widgets/spinner.py index 2940feb594..83aac25a59 100644 --- a/esphome/components/lvgl/widgets/spinner.py +++ b/esphome/components/lvgl/widgets/spinner.py @@ -2,7 +2,7 @@ import esphome.config_validation as cv from esphome.cpp_generator import MockObjClass from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME -from ..lv_validation import angle +from ..lv_validation import lv_angle_degrees, lv_milliseconds from ..lvcode import lv_expr from ..types import LvType from . import Widget, WidgetType @@ -12,8 +12,8 @@ CONF_SPINNER = "spinner" SPINNER_SCHEMA = cv.Schema( { - cv.Required(CONF_ARC_LENGTH): angle, - cv.Required(CONF_SPIN_TIME): cv.positive_time_period_milliseconds, + cv.Required(CONF_ARC_LENGTH): lv_angle_degrees, + cv.Required(CONF_SPIN_TIME): lv_milliseconds, } ) @@ -34,9 +34,9 @@ class SpinnerType(WidgetType): def get_uses(self): return (CONF_ARC,) - def obj_creator(self, parent: MockObjClass, config: dict): - spin_time = config[CONF_SPIN_TIME].total_milliseconds - arc_length = config[CONF_ARC_LENGTH] // 10 + async def obj_creator(self, parent: MockObjClass, config: dict): + spin_time = await lv_milliseconds.process(config[CONF_SPIN_TIME]) + arc_length = await lv_angle_degrees.process(config[CONF_ARC_LENGTH]) return lv_expr.call("spinner_create", parent, spin_time, arc_length) diff --git a/esphome/components/lvgl/widgets/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/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index 42cf486e1c..e8931bab7c 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -87,12 +87,12 @@ class TabviewType(WidgetType): ) as content_obj: await set_obj_properties(Widget(content_obj, obj_spec), content_style) - def obj_creator(self, parent: MockObjClass, config: dict): + async def obj_creator(self, parent: MockObjClass, config: dict): return lv_expr.call( "tabview_create", parent, - literal(config[CONF_POSITION]), - literal(config[CONF_SIZE]), + await DIRECTIONS.process(config[CONF_POSITION]), + await size.process(config[CONF_SIZE]), ) diff --git a/esphome/components/m5stack_8angle/m5stack_8angle.cpp b/esphome/components/m5stack_8angle/m5stack_8angle.cpp index 416b903816..c542b4459e 100644 --- a/esphome/components/m5stack_8angle/m5stack_8angle.cpp +++ b/esphome/components/m5stack_8angle/m5stack_8angle.cpp @@ -8,7 +8,6 @@ namespace m5stack_8angle { static const char *const TAG = "m5stack_8angle"; void M5Stack8AngleComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); i2c::ErrorCode err; err = this->read(nullptr, 0); diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index b2bcde98ec..f7a1d622a1 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -28,9 +28,10 @@ CONF_HAS_PULLDOWNS = "has_pulldowns" def check_keys(obj): - if CONF_KEYS in obj: - if len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len(obj[CONF_COLUMNS]): - raise cv.Invalid("The number of key codes must equal the number of buttons") + if CONF_KEYS in obj and len(obj[CONF_KEYS]) != len(obj[CONF_ROWS]) * len( + obj[CONF_COLUMNS] + ): + raise cv.Invalid("The number of key codes must equal the number of buttons") return obj diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index dc61babc7e..8f486de6b7 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -41,8 +41,6 @@ void MAX17043Component::update() { } void MAX17043Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - uint16_t config_reg; if (this->write(&MAX17043_CONFIG, 1) != i2c::ERROR_OK) { this->status_set_warning(); diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 26fba428cc..b5be3106cf 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -19,10 +19,7 @@ void MAX31855Sensor::update() { this->set_timeout("value", 220, f); } -void MAX31855Sensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); - this->spi_setup(); -} +void MAX31855Sensor::setup() { this->spi_setup(); } void MAX31855Sensor::dump_config() { ESP_LOGCONFIG(TAG, "MAX31855:"); LOG_PIN(" CS Pin: ", this->cs_); diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index c30e2e1a31..cc573cbc53 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -11,14 +11,10 @@ static const char *const TAG = "max31856"; // Based on Adafruit's library: https://github.com/adafruit/Adafruit_MAX31856 void MAX31856Sensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); this->spi_setup(); // assert on any fault - ESP_LOGCONFIG(TAG, "Setting up assertion on all faults"); this->write_register_(MAX31856_MASK_REG, 0x0); - - ESP_LOGCONFIG(TAG, "Setting up open circuit fault detection"); this->write_register_(MAX31856_CR0_REG, MAX31856_CR0_OCFAULT01); this->set_thermocouple_type_(); diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index 4c9a4ae540..a9c5204cf5 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -65,7 +65,6 @@ void MAX31865Sensor::update() { } void MAX31865Sensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); this->spi_setup(); // Build base configuration diff --git a/esphome/components/max44009/max44009.cpp b/esphome/components/max44009/max44009.cpp index 6d1ce351d4..928fc47696 100644 --- a/esphome/components/max44009/max44009.cpp +++ b/esphome/components/max44009/max44009.cpp @@ -21,7 +21,6 @@ static const uint8_t MAX44009_ERROR_HIGH_BYTE = -30; static const uint8_t MAX44009_ERROR_LOW_BYTE = -31; void MAX44009Sensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); bool state_ok = false; if (this->mode_ == MAX44009Mode::MAX44009_MODE_LOW_POWER) { state_ok = this->set_low_power_mode(); diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp index a2881911f2..54e0330ff7 100644 --- a/esphome/components/max6675/max6675.cpp +++ b/esphome/components/max6675/max6675.cpp @@ -17,10 +17,7 @@ void MAX6675Sensor::update() { this->set_timeout("value", 250, f); } -void MAX6675Sensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); - this->spi_setup(); -} +void MAX6675Sensor::setup() { this->spi_setup(); } void MAX6675Sensor::dump_config() { LOG_SENSOR("", "MAX6675", this); LOG_PIN(" CS Pin: ", this->cs_); diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp index 5a1da9dc6f..a377a1a192 100644 --- a/esphome/components/max6956/max6956.cpp +++ b/esphome/components/max6956/max6956.cpp @@ -20,7 +20,6 @@ const uint8_t MASK_CURRENT_PIN = 0x0F; * MAX6956 * **************************************/ void MAX6956::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t configuration; if (!this->read_reg_(MAX6956_CONFIGURATION, &configuration)) { this->mark_failed(); diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index 3f78b35bbb..157b317c02 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -116,7 +116,6 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } void MAX7219Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT for (uint8_t i = 0; i < this->num_chips_ * 8; i++) 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/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index 1721dc80ce..9b9921d2f0 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -26,7 +26,6 @@ constexpr uint8_t MAX7219_DISPLAY_TEST = 0x01; float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } void MAX7219Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->stepsleft_ = 0; for (int chip_line = 0; chip_line < this->num_chip_lines_; chip_line++) { diff --git a/esphome/components/max9611/max9611.cpp b/esphome/components/max9611/max9611.cpp index e61a30ab99..f00f9d76be 100644 --- a/esphome/components/max9611/max9611.cpp +++ b/esphome/components/max9611/max9611.cpp @@ -31,7 +31,6 @@ static const float TEMP_LSB = 0.48; // 0.48C/LSB static const float MICRO_VOLTS_PER_VOLT = 1000000.0; void MAX9611Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // Perform dummy-read uint8_t value; this->read(&value, 1); diff --git a/esphome/components/mcp23008/__init__.py b/esphome/components/mcp23008/__init__.py index ed48eb06a6..8ff938114a 100644 --- a/esphome/components/mcp23008/__init__.py +++ b/esphome/components/mcp23008/__init__.py @@ -24,5 +24,5 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = await mcp23xxx_base.register_mcp23xxx(config) + var = await mcp23xxx_base.register_mcp23xxx(config, mcp23x08_base.NUM_PINS) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp index b93bec9e79..0c34e4971a 100644 --- a/esphome/components/mcp23008/mcp23008.cpp +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -7,7 +7,6 @@ namespace mcp23008 { static const char *const TAG = "mcp23008"; void MCP23008::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t iocon; if (!this->read_reg(mcp23x08_base::MCP23X08_IOCON, &iocon)) { this->mark_failed(); diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index 17647e9915..9d8d6e4dae 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -8,7 +8,6 @@ namespace mcp23016 { static const char *const TAG = "mcp23016"; void MCP23016::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t iocon; if (!this->read_reg_(MCP23016_IOCON0, &iocon)) { this->mark_failed(); diff --git a/esphome/components/mcp23017/__init__.py b/esphome/components/mcp23017/__init__.py index 33b8a680cf..e5cc1856eb 100644 --- a/esphome/components/mcp23017/__init__.py +++ b/esphome/components/mcp23017/__init__.py @@ -24,5 +24,5 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = await mcp23xxx_base.register_mcp23xxx(config) + var = await mcp23xxx_base.register_mcp23xxx(config, mcp23x17_base.NUM_PINS) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index 5c0c2c4703..1ad2036939 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -7,7 +7,6 @@ namespace mcp23017 { static const char *const TAG = "mcp23017"; void MCP23017::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t iocon; if (!this->read_reg(mcp23x17_base::MCP23X17_IOCONA, &iocon)) { this->mark_failed(); diff --git a/esphome/components/mcp23s08/__init__.py b/esphome/components/mcp23s08/__init__.py index c6152d58c0..3d4e304f9b 100644 --- a/esphome/components/mcp23s08/__init__.py +++ b/esphome/components/mcp23s08/__init__.py @@ -27,6 +27,6 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = await mcp23xxx_base.register_mcp23xxx(config) + var = await mcp23xxx_base.register_mcp23xxx(config, mcp23x08_base.NUM_PINS) cg.add(var.set_device_address(config[CONF_DEVICEADDRESS])) await spi.register_spi_device(var, config) diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp index 671506c79d..3d944b45d5 100644 --- a/esphome/components/mcp23s08/mcp23s08.cpp +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -13,7 +13,6 @@ void MCP23S08::set_device_address(uint8_t device_addr) { } void MCP23S08::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->enable(); diff --git a/esphome/components/mcp23s17/__init__.py b/esphome/components/mcp23s17/__init__.py index 9a763d09b0..ea8433af2e 100644 --- a/esphome/components/mcp23s17/__init__.py +++ b/esphome/components/mcp23s17/__init__.py @@ -27,6 +27,6 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = await mcp23xxx_base.register_mcp23xxx(config) + var = await mcp23xxx_base.register_mcp23xxx(config, mcp23x17_base.NUM_PINS) cg.add(var.set_device_address(config[CONF_DEVICEADDRESS])) await spi.register_spi_device(var, config) diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp index 1b922a8130..1624eda9e4 100644 --- a/esphome/components/mcp23s17/mcp23s17.cpp +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -13,7 +13,6 @@ void MCP23S17::set_device_address(uint8_t device_addr) { } void MCP23S17::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->enable(); diff --git a/esphome/components/mcp23x08_base/__init__.py b/esphome/components/mcp23x08_base/__init__.py index ba44917202..a3c12165f0 100644 --- a/esphome/components/mcp23x08_base/__init__.py +++ b/esphome/components/mcp23x08_base/__init__.py @@ -4,5 +4,7 @@ from esphome.components import mcp23xxx_base AUTO_LOAD = ["mcp23xxx_base"] CODEOWNERS = ["@jesserockz"] +NUM_PINS = 8 + mcp23x08_base_ns = cg.esphome_ns.namespace("mcp23x08_base") MCP23X08Base = mcp23x08_base_ns.class_("MCP23X08Base", mcp23xxx_base.MCP23XXXBase) diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index 0c20e902c4..e4fb51174b 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -6,19 +6,21 @@ namespace mcp23x08_base { static const char *const TAG = "mcp23x08_base"; -bool MCP23X08Base::digital_read(uint8_t pin) { - uint8_t bit = pin % 8; - uint8_t reg_addr = mcp23x08_base::MCP23X08_GPIO; - uint8_t value = 0; - this->read_reg(reg_addr, &value); - return value & (1 << bit); +bool MCP23X08Base::digital_read_hw(uint8_t pin) { + if (!this->read_reg(mcp23x08_base::MCP23X08_GPIO, &this->input_mask_)) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + return false; + } + return true; } -void MCP23X08Base::digital_write(uint8_t pin, bool value) { +void MCP23X08Base::digital_write_hw(uint8_t pin, bool value) { uint8_t reg_addr = mcp23x08_base::MCP23X08_OLAT; this->update_reg(pin, value, reg_addr); } +bool MCP23X08Base::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) { uint8_t iodir = mcp23x08_base::MCP23X08_IODIR; uint8_t gppu = mcp23x08_base::MCP23X08_GPPU; diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.h b/esphome/components/mcp23x08_base/mcp23x08_base.h index 910519119b..6eee8274b1 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.h +++ b/esphome/components/mcp23x08_base/mcp23x08_base.h @@ -1,7 +1,7 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/mcp23xxx_base/mcp23xxx_base.h" +#include "esphome/core/component.h" #include "esphome/core/hal.h" namespace esphome { @@ -22,10 +22,12 @@ enum MCP23S08GPIORegisters { MCP23X08_OLAT = 0x0A, }; -class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase { +class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase<8> { public: - bool digital_read(uint8_t pin) override; - void digital_write(uint8_t pin, bool value) override; + bool digital_read_hw(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + bool digital_read_cache(uint8_t pin) override; + void pin_mode(uint8_t pin, gpio::Flags flags) override; void pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) override; @@ -33,6 +35,9 @@ class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase { void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) override; uint8_t olat_{0x00}; + + /// State read in digital_read_hw + uint8_t input_mask_{0x00}; }; } // namespace mcp23x08_base diff --git a/esphome/components/mcp23x17_base/__init__.py b/esphome/components/mcp23x17_base/__init__.py index 97e0b3823d..1b93d16ff3 100644 --- a/esphome/components/mcp23x17_base/__init__.py +++ b/esphome/components/mcp23x17_base/__init__.py @@ -4,5 +4,7 @@ from esphome.components import mcp23xxx_base AUTO_LOAD = ["mcp23xxx_base"] CODEOWNERS = ["@jesserockz"] +NUM_PINS = 16 + mcp23x17_base_ns = cg.esphome_ns.namespace("mcp23x17_base") MCP23X17Base = mcp23x17_base_ns.class_("MCP23X17Base", mcp23xxx_base.MCP23XXXBase) diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index 99064f8880..020b8a5ddf 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -1,4 +1,5 @@ #include "mcp23x17_base.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -6,19 +7,31 @@ namespace mcp23x17_base { static const char *const TAG = "mcp23x17_base"; -bool MCP23X17Base::digital_read(uint8_t pin) { - uint8_t bit = pin % 8; - uint8_t reg_addr = pin < 8 ? mcp23x17_base::MCP23X17_GPIOA : mcp23x17_base::MCP23X17_GPIOB; - uint8_t value = 0; - this->read_reg(reg_addr, &value); - return value & (1 << bit); +bool MCP23X17Base::digital_read_hw(uint8_t pin) { + uint8_t data; + if (pin < 8) { + if (!this->read_reg(mcp23x17_base::MCP23X17_GPIOA, &data)) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + return false; + } + this->input_mask_ = encode_uint16(this->input_mask_ >> 8, data); + } else { + if (!this->read_reg(mcp23x17_base::MCP23X17_GPIOB, &data)) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + return false; + } + this->input_mask_ = encode_uint16(data, this->input_mask_ & 0xFF); + } + return true; } -void MCP23X17Base::digital_write(uint8_t pin, bool value) { +void MCP23X17Base::digital_write_hw(uint8_t pin, bool value) { uint8_t reg_addr = pin < 8 ? mcp23x17_base::MCP23X17_OLATA : mcp23x17_base::MCP23X17_OLATB; this->update_reg(pin, value, reg_addr); } +bool MCP23X17Base::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) { uint8_t iodir = pin < 8 ? mcp23x17_base::MCP23X17_IODIRA : mcp23x17_base::MCP23X17_IODIRB; uint8_t gppu = pin < 8 ? mcp23x17_base::MCP23X17_GPPUA : mcp23x17_base::MCP23X17_GPPUB; diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.h b/esphome/components/mcp23x17_base/mcp23x17_base.h index 3d50ee8c03..bdd66503e2 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.h +++ b/esphome/components/mcp23x17_base/mcp23x17_base.h @@ -1,7 +1,7 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/mcp23xxx_base/mcp23xxx_base.h" +#include "esphome/core/component.h" #include "esphome/core/hal.h" namespace esphome { @@ -34,10 +34,12 @@ enum MCP23X17GPIORegisters { MCP23X17_OLATB = 0x15, }; -class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase { +class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase<16> { public: - bool digital_read(uint8_t pin) override; - void digital_write(uint8_t pin, bool value) override; + bool digital_read_hw(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + bool digital_read_cache(uint8_t pin) override; + void pin_mode(uint8_t pin, gpio::Flags flags) override; void pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) override; @@ -46,6 +48,9 @@ class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase { uint8_t olat_a_{0x00}; uint8_t olat_b_{0x00}; + + /// State read in digital_read_hw + uint16_t input_mask_{0x00}; }; } // namespace mcp23x17_base diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index 8cf0ebcd44..d6e82101ad 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -12,8 +12,9 @@ from esphome.const import ( CONF_OUTPUT, CONF_PULLUP, ) -from esphome.core import coroutine +from esphome.core import CORE, ID, coroutine +AUTO_LOAD = ["gpio_expander"] CODEOWNERS = ["@jesserockz"] mcp23xxx_base_ns = cg.esphome_ns.namespace("mcp23xxx_base") @@ -36,9 +37,11 @@ MCP23XXX_CONFIG_SCHEMA = cv.Schema( @coroutine -async def register_mcp23xxx(config): - var = cg.new_Pvariable(config[CONF_ID]) +async def register_mcp23xxx(config, num_pins): + id: ID = config[CONF_ID] + var = cg.new_Pvariable(id) await cg.register_component(var, config) + CORE.data.setdefault(CONF_MCP23XXX, {})[id.id] = num_pins cg.add(var.set_open_drain_ints(config[CONF_OPEN_DRAIN_INTERRUPT])) return var @@ -73,9 +76,12 @@ MCP23XXX_PIN_SCHEMA = pins.gpio_base_schema( @pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23XXX, MCP23XXX_PIN_SCHEMA) async def mcp23xxx_pin_to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - parent = await cg.get_variable(config[CONF_MCP23XXX]) + parent_id: ID = config[CONF_MCP23XXX] + parent = await cg.get_variable(parent_id) + num_pins = cg.TemplateArguments(CORE.data[CONF_MCP23XXX][parent_id.id]) + + var = cg.new_Pvariable(config[CONF_ID], num_pins) cg.add(var.set_parent(parent)) num = config[CONF_NUMBER] diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index fc49f216ee..81324e794f 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -1,24 +1,27 @@ #include "mcp23xxx_base.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { namespace mcp23xxx_base { -float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; } - -void MCP23XXXGPIOPin::setup() { - pin_mode(flags_); +template void MCP23XXXGPIOPin::setup() { + this->pin_mode(flags_); this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); } - -void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } -bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } -void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string MCP23XXXGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via MCP23XXX", pin_); - return buffer; +template void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +template bool MCP23XXXGPIOPin::digital_read() { + return this->parent_->digital_read(this->pin_) != this->inverted_; } +template void MCP23XXXGPIOPin::digital_write(bool value) { + this->parent_->digital_write(this->pin_, value != this->inverted_); +} +template std::string MCP23XXXGPIOPin::dump_summary() const { + return str_snprintf("%u via MCP23XXX", 15, pin_); +} + +template class MCP23XXXGPIOPin<8>; +template class MCP23XXXGPIOPin<16>; } // namespace mcp23xxx_base } // namespace esphome diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index 9686c9fd33..ab7f8ec398 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/components/gpio_expander/cached_gpio.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" @@ -8,15 +9,15 @@ namespace mcp23xxx_base { enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING }; -class MCP23XXXBase : public Component { +template class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander { public: - virtual bool digital_read(uint8_t pin); - virtual void digital_write(uint8_t pin, bool value); virtual void pin_mode(uint8_t pin, gpio::Flags flags); virtual void pin_interrupt_mode(uint8_t pin, MCP23XXXInterruptMode interrupt_mode); void set_open_drain_ints(const bool value) { this->open_drain_ints_ = value; } - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::IO; } + + void loop() override { this->reset_pin_cache_(); } protected: // read a given register @@ -29,7 +30,7 @@ class MCP23XXXBase : public Component { bool open_drain_ints_; }; -class MCP23XXXGPIOPin : public GPIOPin { +template class MCP23XXXGPIOPin : public GPIOPin { public: void setup() override; void pin_mode(gpio::Flags flags) override; @@ -37,7 +38,7 @@ class MCP23XXXGPIOPin : public GPIOPin { void digital_write(bool value) override; std::string dump_summary() const override; - void set_parent(MCP23XXXBase *parent) { parent_ = parent; } + void set_parent(MCP23XXXBase *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } void set_flags(gpio::Flags flags) { flags_ = flags; } @@ -46,7 +47,7 @@ class MCP23XXXGPIOPin : public GPIOPin { gpio::Flags get_flags() const override { return this->flags_; } protected: - MCP23XXXBase *parent_; + MCP23XXXBase *parent_; uint8_t pin_; bool inverted_; gpio::Flags flags_; diff --git a/esphome/components/mcp3008/mcp3008.cpp b/esphome/components/mcp3008/mcp3008.cpp index fb9bda35d0..812a3b0c83 100644 --- a/esphome/components/mcp3008/mcp3008.cpp +++ b/esphome/components/mcp3008/mcp3008.cpp @@ -10,10 +10,7 @@ static const char *const TAG = "mcp3008"; float MCP3008::get_setup_priority() const { return setup_priority::HARDWARE; } -void MCP3008::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->spi_setup(); -} +void MCP3008::setup() { this->spi_setup(); } void MCP3008::dump_config() { ESP_LOGCONFIG(TAG, "MCP3008:"); diff --git a/esphome/components/mcp3204/mcp3204.cpp b/esphome/components/mcp3204/mcp3204.cpp index 1f956612d7..4bb0cbed76 100644 --- a/esphome/components/mcp3204/mcp3204.cpp +++ b/esphome/components/mcp3204/mcp3204.cpp @@ -8,10 +8,7 @@ static const char *const TAG = "mcp3204"; float MCP3204::get_setup_priority() const { return setup_priority::HARDWARE; } -void MCP3204::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->spi_setup(); -} +void MCP3204::setup() { this->spi_setup(); } void MCP3204::dump_config() { ESP_LOGCONFIG(TAG, "MCP3204:"); diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index 39127a6c04..6634c5057e 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -10,7 +10,6 @@ static const char *const TAG = "mcp4461"; constexpr uint8_t EEPROM_WRITE_TIMEOUT_MS = 10; void Mcp4461Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup for address 0x%02X", this->address_); auto err = this->write(nullptr, 0); if (err != i2c::ERROR_OK) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; diff --git a/esphome/components/mcp4725/mcp4725.cpp b/esphome/components/mcp4725/mcp4725.cpp index 8b2f8524d8..137ac9cb61 100644 --- a/esphome/components/mcp4725/mcp4725.cpp +++ b/esphome/components/mcp4725/mcp4725.cpp @@ -7,7 +7,6 @@ namespace mcp4725 { static const char *const TAG = "mcp4725"; void MCP4725::setup() { - ESP_LOGCONFIG(TAG, "Running setup for address 0x%02X", this->address_); auto err = this->write(nullptr, 0); if (err != i2c::ERROR_OK) { this->error_code_ = COMMUNICATION_FAILED; diff --git a/esphome/components/mcp4728/mcp4728.cpp b/esphome/components/mcp4728/mcp4728.cpp index 7b2b43d4d8..bab94cb233 100644 --- a/esphome/components/mcp4728/mcp4728.cpp +++ b/esphome/components/mcp4728/mcp4728.cpp @@ -9,7 +9,6 @@ namespace mcp4728 { static const char *const TAG = "mcp4728"; void MCP4728Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup for address 0x%02X", this->address_); auto err = this->write(nullptr, 0); if (err != i2c::ERROR_OK) { this->mark_failed(); diff --git a/esphome/components/mcp9600/mcp9600.cpp b/esphome/components/mcp9600/mcp9600.cpp index 16c19326f2..e1a88988c4 100644 --- a/esphome/components/mcp9600/mcp9600.cpp +++ b/esphome/components/mcp9600/mcp9600.cpp @@ -28,8 +28,6 @@ static const uint8_t MCP9600_REGISTER_ALERT4_LIMIT = 0x13; static const uint8_t MCP9600_REGISTER_DEVICE_ID = 0x20; void MCP9600Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - uint16_t dev_id = 0; this->read_byte_16(MCP9600_REGISTER_DEVICE_ID, &dev_id); this->device_id_ = (uint8_t) (dev_id >> 8); diff --git a/esphome/components/mcp9808/mcp9808.cpp b/esphome/components/mcp9808/mcp9808.cpp index 02ddc1aceb..088d33887f 100644 --- a/esphome/components/mcp9808/mcp9808.cpp +++ b/esphome/components/mcp9808/mcp9808.cpp @@ -18,8 +18,6 @@ static const uint8_t MCP9808_AMBIENT_TEMP_NEGATIVE = 0x10; static const char *const TAG = "mcp9808"; void MCP9808Sensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); - uint16_t manu = 0; if (!this->read_byte_16(MCP9808_REG_MANUF_ID, &manu) || manu != MCP9808_MANUF_ID) { this->mark_failed(); diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index e32d39cede..469fe8ada6 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -93,6 +93,9 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + if config[CONF_SERVICES]: + cg.add_define("USE_MDNS_EXTRA_SERVICES") + for service in config[CONF_SERVICES]: txt = [ cg.StructInitializer( diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 06ca99b402..640750720d 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -104,7 +104,9 @@ void MDNSComponent::compile_records_() { } #endif +#ifdef USE_MDNS_EXTRA_SERVICES this->services_.insert(this->services_.end(), this->services_extra_.begin(), this->services_extra_.end()); +#endif if (this->services_.empty()) { // Publish "http" service if not using native API diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 93a16f40d2..f87ef08bcd 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -35,14 +35,18 @@ class MDNSComponent : public Component { #endif float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } +#ifdef USE_MDNS_EXTRA_SERVICES void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); } +#endif std::vector get_services(); void on_shutdown() override; protected: +#ifdef USE_MDNS_EXTRA_SERVICES std::vector services_extra_{}; +#endif std::vector services_{}; std::string hostname_; void compile_records_(); diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index ccded1deb2..d288e70cba 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -7,6 +7,8 @@ from esphome.const import ( CONF_ID, CONF_ON_IDLE, CONF_ON_STATE, + CONF_ON_TURN_OFF, + CONF_ON_TURN_ON, CONF_TRIGGER_ID, CONF_VOLUME, ) @@ -58,6 +60,12 @@ VolumeDownAction = media_player_ns.class_( VolumeSetAction = media_player_ns.class_( "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) ) +TurnOnAction = media_player_ns.class_( + "TurnOnAction", automation.Action, cg.Parented.template(MediaPlayer) +) +TurnOffAction = media_player_ns.class_( + "TurnOffAction", automation.Action, cg.Parented.template(MediaPlayer) +) CONF_ANNOUNCEMENT = "announcement" CONF_ON_PLAY = "on_play" @@ -72,12 +80,16 @@ PauseTrigger = media_player_ns.class_("PauseTrigger", automation.Trigger.templat AnnoucementTrigger = media_player_ns.class_( "AnnouncementTrigger", automation.Trigger.template() ) +OnTrigger = media_player_ns.class_("OnTrigger", automation.Trigger.template()) +OffTrigger = media_player_ns.class_("OffTrigger", automation.Trigger.template()) IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition) IsPausedCondition = media_player_ns.class_("IsPausedCondition", automation.Condition) IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition) IsAnnouncingCondition = media_player_ns.class_( "IsAnnouncingCondition", automation.Condition ) +IsOnCondition = media_player_ns.class_("IsOnCondition", automation.Condition) +IsOffCondition = media_player_ns.class_("IsOffCondition", automation.Condition) async def setup_media_player_core_(var, config): @@ -97,6 +109,12 @@ async def setup_media_player_core_(var, config): for conf in config.get(CONF_ON_ANNOUNCEMENT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_TURN_ON, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_TURN_OFF, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) async def register_media_player(var, config): @@ -140,6 +158,16 @@ _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(AnnoucementTrigger), } ), + cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnTrigger), + } + ), + cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OffTrigger), + } + ), } ) @@ -218,6 +246,12 @@ async def media_player_play_media_action(config, action_id, template_arg, args): @automation.register_action( "media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA ) +@automation.register_action( + "media_player.turn_on", TurnOnAction, MEDIA_PLAYER_ACTION_SCHEMA +) +@automation.register_action( + "media_player.turn_off", TurnOffAction, MEDIA_PLAYER_ACTION_SCHEMA +) async def media_player_action(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) @@ -238,6 +272,12 @@ async def media_player_action(config, action_id, template_arg, args): @automation.register_condition( "media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_CONDITION_SCHEMA ) +@automation.register_condition( + "media_player.is_on", IsOnCondition, MEDIA_PLAYER_CONDITION_SCHEMA +) +@automation.register_condition( + "media_player.is_off", IsOffCondition, MEDIA_PLAYER_CONDITION_SCHEMA +) async def media_player_condition(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 422c224a85..3af5959f32 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -28,6 +28,10 @@ template using VolumeUpAction = MediaPlayerCommandAction; template using VolumeDownAction = MediaPlayerCommandAction; +template +using TurnOnAction = MediaPlayerCommandAction; +template +using TurnOffAction = MediaPlayerCommandAction; template class PlayMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) @@ -66,6 +70,8 @@ using IdleTrigger = MediaPlayerStateTrigger; using PauseTrigger = MediaPlayerStateTrigger; using AnnouncementTrigger = MediaPlayerStateTrigger; +using OnTrigger = MediaPlayerStateTrigger; +using OffTrigger = MediaPlayerStateTrigger; template class IsIdleCondition : public Condition, public Parented { public: @@ -87,5 +93,15 @@ template class IsAnnouncingCondition : public Condition, bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; } }; +template class IsOnCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ON; } +}; + +template class IsOffCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; } +}; + } // namespace media_player } // namespace esphome diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 32da7ee265..3f274bf73b 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -9,6 +9,10 @@ static const char *const TAG = "media_player"; const char *media_player_state_to_string(MediaPlayerState state) { switch (state) { + case MEDIA_PLAYER_STATE_ON: + return "ON"; + case MEDIA_PLAYER_STATE_OFF: + return "OFF"; case MEDIA_PLAYER_STATE_IDLE: return "IDLE"; case MEDIA_PLAYER_STATE_PLAYING: @@ -18,6 +22,7 @@ const char *media_player_state_to_string(MediaPlayerState state) { case MEDIA_PLAYER_STATE_ANNOUNCING: return "ANNOUNCING"; case MEDIA_PLAYER_STATE_NONE: + return "NONE"; default: return "UNKNOWN"; } @@ -49,6 +54,10 @@ const char *media_player_command_to_string(MediaPlayerCommand command) { return "REPEAT_OFF"; case MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST: return "CLEAR_PLAYLIST"; + case MEDIA_PLAYER_COMMAND_TURN_ON: + return "TURN_ON"; + case MEDIA_PLAYER_COMMAND_TURN_OFF: + return "TURN_OFF"; default: return "UNKNOWN"; } @@ -110,6 +119,10 @@ MediaPlayerCall &MediaPlayerCall::set_command(const std::string &command) { this->set_command(MEDIA_PLAYER_COMMAND_UNMUTE); } else if (str_equals_case_insensitive(command, "TOGGLE")) { this->set_command(MEDIA_PLAYER_COMMAND_TOGGLE); + } else if (str_equals_case_insensitive(command, "TURN_ON")) { + this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON); + } else if (str_equals_case_insensitive(command, "TURN_OFF")) { + this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF); } else { ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command.c_str()); } diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index ee5889901c..2f1c99115f 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -6,12 +6,40 @@ namespace esphome { namespace media_player { +enum MediaPlayerEntityFeature : uint32_t { + PAUSE = 1 << 0, + SEEK = 1 << 1, + VOLUME_SET = 1 << 2, + VOLUME_MUTE = 1 << 3, + PREVIOUS_TRACK = 1 << 4, + NEXT_TRACK = 1 << 5, + + TURN_ON = 1 << 7, + TURN_OFF = 1 << 8, + PLAY_MEDIA = 1 << 9, + VOLUME_STEP = 1 << 10, + SELECT_SOURCE = 1 << 11, + STOP = 1 << 12, + CLEAR_PLAYLIST = 1 << 13, + PLAY = 1 << 14, + SHUFFLE_SET = 1 << 15, + SELECT_SOUND_MODE = 1 << 16, + BROWSE_MEDIA = 1 << 17, + REPEAT_SET = 1 << 18, + GROUPING = 1 << 19, + MEDIA_ANNOUNCE = 1 << 20, + MEDIA_ENQUEUE = 1 << 21, + SEARCH_MEDIA = 1 << 22, +}; + enum MediaPlayerState : uint8_t { MEDIA_PLAYER_STATE_NONE = 0, MEDIA_PLAYER_STATE_IDLE = 1, MEDIA_PLAYER_STATE_PLAYING = 2, MEDIA_PLAYER_STATE_PAUSED = 3, - MEDIA_PLAYER_STATE_ANNOUNCING = 4 + MEDIA_PLAYER_STATE_ANNOUNCING = 4, + MEDIA_PLAYER_STATE_OFF = 5, + MEDIA_PLAYER_STATE_ON = 6, }; const char *media_player_state_to_string(MediaPlayerState state); @@ -28,6 +56,8 @@ enum MediaPlayerCommand : uint8_t { MEDIA_PLAYER_COMMAND_REPEAT_ONE = 9, MEDIA_PLAYER_COMMAND_REPEAT_OFF = 10, MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11, + MEDIA_PLAYER_COMMAND_TURN_ON = 12, + MEDIA_PLAYER_COMMAND_TURN_OFF = 13, }; const char *media_player_command_to_string(MediaPlayerCommand command); @@ -51,14 +81,31 @@ class MediaPlayerTraits { MediaPlayerTraits() = default; void set_supports_pause(bool supports_pause) { this->supports_pause_ = supports_pause; } - bool get_supports_pause() const { return this->supports_pause_; } + void set_supports_turn_off_on(bool supports_turn_off_on) { this->supports_turn_off_on_ = supports_turn_off_on; } + bool get_supports_turn_off_on() const { return this->supports_turn_off_on_; } + std::vector &get_supported_formats() { return this->supported_formats_; } + uint32_t get_feature_flags() const { + uint32_t flags = 0; + flags |= MediaPlayerEntityFeature::PLAY_MEDIA | MediaPlayerEntityFeature::BROWSE_MEDIA | + MediaPlayerEntityFeature::STOP | MediaPlayerEntityFeature::VOLUME_SET | + MediaPlayerEntityFeature::VOLUME_MUTE | MediaPlayerEntityFeature::MEDIA_ANNOUNCE; + if (this->get_supports_pause()) { + flags |= MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY; + } + if (this->get_supports_turn_off_on()) { + flags |= MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON; + } + return flags; + } + protected: - bool supports_pause_{false}; std::vector supported_formats_{}; + bool supports_pause_{false}; + bool supports_turn_off_on_{false}; }; class MediaPlayerCall { diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 201d956a37..6fca48a5bd 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -72,8 +72,6 @@ void MicroWakeWord::dump_config() { } void MicroWakeWord::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->frontend_config_.window.size_ms = FEATURE_DURATION_MS; this->frontend_config_.window.step_size_ms = this->features_step_size_; this->frontend_config_.filterbank.num_channels = PREPROCESSOR_FEATURE_SIZE; @@ -130,7 +128,6 @@ void MicroWakeWord::setup() { } }); #endif - ESP_LOGCONFIG(TAG, "Micro Wake Word initialized"); } void MicroWakeWord::inference_task(void *params) { diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp index 3a2cf22914..8181ece94c 100644 --- a/esphome/components/mics_4514/mics_4514.cpp +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -12,7 +12,6 @@ static const uint8_t SENSOR_REGISTER = 0x04; static const uint8_t POWER_MODE_REGISTER = 0x0a; void MICS4514Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t power_mode; this->read_register(POWER_MODE_REGISTER, &power_mode, 1); if (power_mode == 0x00) { @@ -20,16 +19,12 @@ void MICS4514Component::setup() { power_mode = 0x01; this->write_register(POWER_MODE_REGISTER, &power_mode, 1); delay(100); // NOLINT - this->set_timeout("warmup", 3 * 60 * 1000, [this]() { - this->warmed_up_ = true; - ESP_LOGCONFIG(TAG, "MICS 4514 setup complete."); - }); + this->set_timeout("warmup", 3 * 60 * 1000, [this]() { this->warmed_up_ = true; }); this->status_set_warning(); return; } ESP_LOGCONFIG(TAG, "Device already awake."); this->warmed_up_ = true; - ESP_LOGCONFIG(TAG, "MICS 4514 setup complete."); } void MICS4514Component::dump_config() { ESP_LOGCONFIG(TAG, "MICS 4514:"); diff --git a/esphome/components/midea/ir_transmitter.h b/esphome/components/midea/ir_transmitter.h index eba8fc87f7..a16aed2e72 100644 --- a/esphome/components/midea/ir_transmitter.h +++ b/esphome/components/midea/ir_transmitter.h @@ -54,15 +54,15 @@ class IrFollowMeData : public IrData { void set_fahrenheit(bool val) { this->set_mask_(2, val, 32); } protected: - static const uint8_t MIN_TEMP_C = 0; - static const uint8_t MAX_TEMP_C = 37; + inline static constexpr uint8_t MIN_TEMP_C = 0; + inline static constexpr uint8_t MAX_TEMP_C = 37; // see // https://github.com/crankyoldgit/IRremoteESP8266/blob/9bdf8abcb465268c5409db99dc83a26df64c7445/src/ir_Midea.h#L116 - static const uint8_t MIN_TEMP_F = 32; + inline static constexpr uint8_t MIN_TEMP_F = 32; // see // https://github.com/crankyoldgit/IRremoteESP8266/blob/9bdf8abcb465268c5409db99dc83a26df64c7445/src/ir_Midea.h#L117 - static const uint8_t MAX_TEMP_F = 99; + inline static constexpr uint8_t MAX_TEMP_F = 99; }; class IrSpecialData : public IrData { diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py new file mode 100644 index 0000000000..f610f160b0 --- /dev/null +++ b/esphome/components/mipi/__init__.py @@ -0,0 +1,416 @@ +# Various constants used in MIPI DBI communication +# Various configuration constants for MIPI displays +# Various utility functions for MIPI DBI configuration + +from typing import Any + +from esphome.components.const import CONF_COLOR_DEPTH +from esphome.components.display import CONF_SHOW_TEST_CARD, display_ns +import esphome.config_validation as cv +from esphome.const import ( + CONF_BRIGHTNESS, + CONF_COLOR_ORDER, + CONF_DIMENSIONS, + CONF_HEIGHT, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_OFFSET_HEIGHT, + CONF_OFFSET_WIDTH, + CONF_PAGES, + CONF_ROTATION, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_WIDTH, +) +from esphome.core import TimePeriod + +LOGGER = cv.logging.getLogger(__name__) + +ColorOrder = display_ns.enum("ColorMode") + +NOP = 0x00 +SWRESET = 0x01 +RDDID = 0x04 +RDDST = 0x09 +RDMODE = 0x0A +RDMADCTL = 0x0B +RDPIXFMT = 0x0C +RDIMGFMT = 0x0D +RDSELFDIAG = 0x0F +SLEEP_IN = 0x10 +SLPIN = 0x10 +SLEEP_OUT = 0x11 +SLPOUT = 0x11 +PTLON = 0x12 +NORON = 0x13 +INVERT_OFF = 0x20 +INVOFF = 0x20 +INVERT_ON = 0x21 +INVON = 0x21 +ALL_ON = 0x23 +WRAM = 0x24 +GAMMASET = 0x26 +MIPI = 0x26 +DISPOFF = 0x28 +DISPON = 0x29 +CASET = 0x2A +PASET = 0x2B +RASET = 0x2B +RAMWR = 0x2C +WDATA = 0x2C +RAMRD = 0x2E +PTLAR = 0x30 +VSCRDEF = 0x33 +TEON = 0x35 +MADCTL = 0x36 +MADCTL_CMD = 0x36 +VSCRSADD = 0x37 +IDMOFF = 0x38 +IDMON = 0x39 +COLMOD = 0x3A +PIXFMT = 0x3A +GETSCANLINE = 0x45 +BRIGHTNESS = 0x51 +WRDISBV = 0x51 +RDDISBV = 0x52 +WRCTRLD = 0x53 +WCE = 0x58 +SWIRE1 = 0x5A +SWIRE2 = 0x5B +IFMODE = 0xB0 +FRMCTR1 = 0xB1 +FRMCTR2 = 0xB2 +FRMCTR3 = 0xB3 +INVCTR = 0xB4 +DFUNCTR = 0xB6 +ETMOD = 0xB7 +PWCTR1 = 0xC0 +PWCTR2 = 0xC1 +PWCTR3 = 0xC2 +PWCTR4 = 0xC3 +PWCTR5 = 0xC4 +SPIMODESEL = 0xC4 +VMCTR1 = 0xC5 +IFCTR = 0xC6 +VMCTR2 = 0xC7 +GMCTR = 0xC8 +SETEXTC = 0xC8 +PWSET = 0xD0 +VMCTR = 0xD1 +PWSETN = 0xD2 +RDID4 = 0xD3 +RDINDEX = 0xD9 +RDID1 = 0xDA +RDID2 = 0xDB +RDID3 = 0xDC +RDIDX = 0xDD +GMCTRP1 = 0xE0 +GMCTRN1 = 0xE1 +CSCON = 0xF0 +PWCTR6 = 0xF6 +ADJCTL3 = 0xF7 +PAGESEL = 0xFE + +MADCTL_MY = 0x80 # Bit 7 Bottom to top +MADCTL_MX = 0x40 # Bit 6 Right to left +MADCTL_MV = 0x20 # Bit 5 Reverse Mode +MADCTL_ML = 0x10 # Bit 4 LCD refresh Bottom to top +MADCTL_RGB = 0x00 # Bit 3 Red-Green-Blue pixel order +MADCTL_BGR = 0x08 # Bit 3 Blue-Green-Red pixel order +MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left + +# These bits are used instead of the above bits on some chips, where using MX and MY results in incorrect +# partial updates. +MADCTL_XFLIP = 0x02 # Mirror the display horizontally +MADCTL_YFLIP = 0x01 # Mirror the display vertically + +# Special constant for delays in command sequences +DELAY_FLAG = 0xFFF # Special flag to indicate a delay + +CONF_PIXEL_MODE = "pixel_mode" +CONF_USE_AXIS_FLIPS = "use_axis_flips" + +PIXEL_MODE_24BIT = "24bit" +PIXEL_MODE_18BIT = "18bit" +PIXEL_MODE_16BIT = "16bit" + +PIXEL_MODES = { + PIXEL_MODE_16BIT: 0x55, + PIXEL_MODE_18BIT: 0x66, + PIXEL_MODE_24BIT: 0x77, +} + +MODE_RGB = "RGB" +MODE_BGR = "BGR" +COLOR_ORDERS = { + MODE_RGB: ColorOrder.COLOR_ORDER_RGB, + MODE_BGR: ColorOrder.COLOR_ORDER_BGR, +} + +CONF_HSYNC_BACK_PORCH = "hsync_back_porch" +CONF_HSYNC_FRONT_PORCH = "hsync_front_porch" +CONF_HSYNC_PULSE_WIDTH = "hsync_pulse_width" +CONF_VSYNC_BACK_PORCH = "vsync_back_porch" +CONF_VSYNC_FRONT_PORCH = "vsync_front_porch" +CONF_VSYNC_PULSE_WIDTH = "vsync_pulse_width" +CONF_PCLK_FREQUENCY = "pclk_frequency" +CONF_PCLK_INVERTED = "pclk_inverted" +CONF_NATIVE_HEIGHT = "native_height" +CONF_NATIVE_WIDTH = "native_width" + +CONF_DE_PIN = "de_pin" +CONF_PCLK_PIN = "pclk_pin" + + +def power_of_two(value): + value = cv.int_range(1, 128)(value) + if value & (value - 1) != 0: + raise cv.Invalid("value must be a power of two") + return value + + +def validate_dimension(rounding): + def validator(value): + value = cv.positive_int(value) + if value % rounding != 0: + raise cv.Invalid(f"Dimensions and offsets must be divisible by {rounding}") + return value + + return validator + + +def dimension_schema(rounding): + return cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): validate_dimension(rounding), + cv.Required(CONF_HEIGHT): validate_dimension(rounding), + cv.Optional(CONF_OFFSET_HEIGHT, default=0): validate_dimension( + rounding + ), + cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding), + } + ), + ) + + +def map_sequence(value): + """ + Maps one entry in a sequence to a command and data bytes. + The format is a repeated sequence of [CMD, ] where is s a sequence of bytes. The length is inferred + from the length of the sequence and should not be explicit. + A single integer can be provided where there are no data bytes, in which case it is treated as a command. + A delay can be inserted by specifying "- delay N" where N is in ms + """ + if isinstance(value, str) and value.lower().startswith("delay "): + value = value.lower()[6:] + delay_value = cv.All( + cv.positive_time_period_milliseconds, + cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)), + )(value) + return DELAY_FLAG, delay_value.total_milliseconds + value = cv.All(cv.ensure_list(cv.int_range(0, 255)), cv.Length(1, 254))(value) + return tuple(value) + + +def delay(ms): + return DELAY_FLAG, ms + + +class DriverChip: + models = {} + + def __init__( + self, + name: str, + initsequence=None, + **defaults, + ): + name = name.upper() + self.name = name + self.initsequence = initsequence or defaults.get("init_sequence") + self.defaults = defaults + DriverChip.models[name] = self + + @classmethod + def get_models(cls): + """ + Return the current set of models and reset the models dictionary. + """ + models = cls.models + cls.models = {} + return models + + def extend(self, name, **kwargs) -> "DriverChip": + defaults = self.defaults.copy() + if ( + CONF_WIDTH in defaults + and CONF_OFFSET_WIDTH in kwargs + and CONF_NATIVE_WIDTH not in defaults + ): + defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH] + if ( + CONF_HEIGHT in defaults + and CONF_OFFSET_HEIGHT in kwargs + and CONF_NATIVE_HEIGHT not in defaults + ): + defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT] + defaults.update(kwargs) + return DriverChip(name, initsequence=self.initsequence, **defaults) + + def get_default(self, key, fallback: Any = False) -> Any: + return self.defaults.get(key, fallback) + + def option(self, name, fallback=False) -> cv.Optional: + return cv.Optional(name, default=self.get_default(name, fallback)) + + def rotation_as_transform(self, config) -> bool: + """ + Check if a rotation can be implemented in hardware using the MADCTL register. + A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. + """ + rotation = config.get(CONF_ROTATION, 0) + return rotation and ( + self.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 + ) + + def get_dimensions(self, config) -> tuple[int, int, int, int]: + if CONF_DIMENSIONS in config: + # Explicit dimensions, just use as is + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + width = dimensions[CONF_WIDTH] + height = dimensions[CONF_HEIGHT] + 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 = self.get_transform(config) + + width = self.get_default(CONF_WIDTH) + height = self.get_default(CONF_HEIGHT) + offset_width = self.get_default(CONF_OFFSET_WIDTH, 0) + offset_height = self.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 = self.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2) + offset_width = native_width - width - offset_width + if transform[CONF_MIRROR_Y]: + native_height = self.get_default( + CONF_NATIVE_HEIGHT, height + offset_height * 2 + ) + offset_height = native_height - height - offset_height + # Swap default dimensions if swap_xy is set + if transform[CONF_SWAP_XY] is True: + width, height = height, width + offset_height, offset_width = offset_width, offset_height + return width, height, offset_width, offset_height + + def get_transform(self, config) -> dict[str, bool]: + can_transform = self.rotation_as_transform(config) + transform = config.get( + CONF_TRANSFORM, + { + CONF_MIRROR_X: self.get_default(CONF_MIRROR_X, False), + CONF_MIRROR_Y: self.get_default(CONF_MIRROR_Y, False), + CONF_SWAP_XY: self.get_default(CONF_SWAP_XY, False), + }, + ) + + # Can we use the MADCTL register to set the rotation? + if can_transform and CONF_TRANSFORM not in config: + rotation = config[CONF_ROTATION] + if rotation == 180: + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + elif rotation == 90: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + else: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform[CONF_TRANSFORM] = True + return transform + + def get_sequence(self, config) -> tuple[tuple[int, ...], int]: + """ + Create the init sequence for the display. + Use the default sequence from the model, if any, and append any custom sequence provided in the config. + Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence + Pixel format, color order, and orientation will be set. + Returns a tuple of the init sequence and the computed MADCTL value. + """ + sequence = list(self.initsequence or ()) + custom_sequence = config.get(CONF_INIT_SEQUENCE, []) + sequence.extend(custom_sequence) + # Ensure each command is a tuple + sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] + + # Set pixel format if not already in the custom sequence + pixel_mode = config[CONF_PIXEL_MODE] + if not isinstance(pixel_mode, int): + if not pixel_mode.endswith("bit"): + pixel_mode = f"{pixel_mode}bit" + pixel_mode = PIXEL_MODES[pixel_mode] + sequence.append((PIXFMT, pixel_mode)) + + # Does the chip use the flipping bits for mirroring rather than the reverse order bits? + use_flip = config.get(CONF_USE_AXIS_FLIPS) + madctl = 0 + transform = self.get_transform(config) + if self.rotation_as_transform(config): + LOGGER.info("Using hardware transform to implement rotation") + if transform.get(CONF_MIRROR_X): + madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX + if transform.get(CONF_MIRROR_Y): + madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY + if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined + madctl |= MADCTL_MV + if config[CONF_COLOR_ORDER] == MODE_BGR: + madctl |= MADCTL_BGR + sequence.append((MADCTL, madctl)) + if config[CONF_INVERT_COLORS]: + sequence.append((INVON,)) + else: + sequence.append((INVOFF,)) + if brightness := config.get(CONF_BRIGHTNESS, self.get_default(CONF_BRIGHTNESS)): + sequence.append((BRIGHTNESS, brightness)) + sequence.append((SLPOUT,)) + sequence.append((DISPON,)) + + # Flatten the sequence into a list of bytes, with the length of each command + # or the delay flag inserted where needed + return sum( + tuple( + (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] + for x in sequence + ), + (), + ), madctl + + +def requires_buffer(config) -> bool: + """ + 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) -> int: + """ + Get the color depth in bits from the configuration. + """ + return int(config[CONF_COLOR_DEPTH].removesuffix("bit")) diff --git a/esphome/components/mipi_dsi/__init__.py b/esphome/components/mipi_dsi/__init__.py new file mode 100644 index 0000000000..84dd66e2f4 --- /dev/null +++ b/esphome/components/mipi_dsi/__init__.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@clydebarrow"] + +mipi_dsi_ns = cg.esphome_ns.namespace("mipi_dsi") diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py new file mode 100644 index 0000000000..4fc837be67 --- /dev/null +++ b/esphome/components/mipi_dsi/display.py @@ -0,0 +1,233 @@ +import importlib +import logging +import pkgutil + +from esphome import pins +import esphome.codegen as cg +from esphome.components import display +from esphome.components.const import ( + BYTE_ORDER_BIG, + BYTE_ORDER_LITTLE, + CONF_BYTE_ORDER, + CONF_DRAW_ROUNDING, +) +from esphome.components.display import CONF_SHOW_TEST_CARD +from esphome.components.esp32 import const, only_on_variant +from esphome.components.mipi import ( + COLOR_ORDERS, + CONF_COLOR_DEPTH, + CONF_HSYNC_BACK_PORCH, + CONF_HSYNC_FRONT_PORCH, + CONF_HSYNC_PULSE_WIDTH, + CONF_PCLK_FREQUENCY, + CONF_PIXEL_MODE, + CONF_USE_AXIS_FLIPS, + CONF_VSYNC_BACK_PORCH, + CONF_VSYNC_FRONT_PORCH, + CONF_VSYNC_PULSE_WIDTH, + MODE_BGR, + PIXEL_MODE_16BIT, + PIXEL_MODE_24BIT, + DriverChip, + dimension_schema, + get_color_depth, + map_sequence, + power_of_two, + requires_buffer, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_COLOR_ORDER, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_ID, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_RESET_PIN, + CONF_ROTATION, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_WIDTH, +) +from esphome.final_validate import full_config + +from . import mipi_dsi_ns, models + +# Currently only ESP32-P4 is supported, so esp_ldo and psram are required +DEPENDENCIES = ["esp32", "esp_ldo", "psram"] +DOMAIN = "mipi_dsi" + +LOGGER = logging.getLogger(DOMAIN) + +MIPI_DSI = mipi_dsi_ns.class_("MIPI_DSI", display.Display, cg.Component) +ColorOrder = display.display_ns.enum("ColorMode") +ColorBitness = display.display_ns.enum("ColorBitness") + +CONF_LANE_BIT_RATE = "lane_bit_rate" +CONF_LANES = "lanes" + +DriverChip("CUSTOM") + +# Import all models dynamically from the models package + +for module_info in pkgutil.iter_modules(models.__path__): + importlib.import_module(f".models.{module_info.name}", package=__package__) + +MODELS = DriverChip.get_models() + +COLOR_DEPTHS = { + 16: ColorBitness.COLOR_BITNESS_565, + 24: ColorBitness.COLOR_BITNESS_888, +} + + +def model_schema(config): + model = MODELS[config[CONF_MODEL].upper()] + transform = cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ) + if model.get_default(CONF_SWAP_XY) != 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) + ) + swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) + + # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden + cv_dimensions = ( + cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required + ) + pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_24BIT, "16", "24") + schema = display.FULL_DISPLAY_SCHEMA.extend( + { + model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(MIPI_DSI), + cv_dimensions(CONF_DIMENSIONS): dimension_schema( + model.get_default(CONF_DRAW_ROUNDING, 1) + ), + model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list( + pins.gpio_output_pin_schema + ), + model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(COLOR_ORDERS, upper=True), + model.option(CONF_DRAW_ROUNDING, 2): power_of_two, + model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of( + *pixel_modes, lower=True + ), + model.option(CONF_TRANSFORM, cv.UNDEFINED): transform, + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + model.option(CONF_INVERT_COLORS, False): cv.boolean, + model.option(CONF_COLOR_DEPTH, "16"): cv.one_of( + *[str(d) for d in COLOR_DEPTHS], + *[f"{d}bit" for d in COLOR_DEPTHS], + lower=True, + ), + model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean, + model.option(CONF_PCLK_FREQUENCY, "40MHz"): cv.All( + cv.frequency, cv.Range(min=4e6, max=100e6) + ), + model.option(CONF_LANES, 2): cv.int_range(1, 2), + model.option(CONF_LANE_BIT_RATE, None): cv.All( + cv.bps, cv.Range(min=100e6, max=3200e6) + ), + iseqconf: cv.ensure_list(map_sequence), + model.option(CONF_BYTE_ORDER, BYTE_ORDER_LITTLE): cv.one_of( + BYTE_ORDER_LITTLE, BYTE_ORDER_BIG, lower=True + ), + model.option(CONF_HSYNC_PULSE_WIDTH): cv.int_, + model.option(CONF_HSYNC_BACK_PORCH): cv.int_, + model.option(CONF_HSYNC_FRONT_PORCH): cv.int_, + model.option(CONF_VSYNC_PULSE_WIDTH): cv.int_, + model.option(CONF_VSYNC_BACK_PORCH): cv.int_, + model.option(CONF_VSYNC_FRONT_PORCH): cv.int_, + } + ) + return cv.All( + schema, + only_on_variant(supported=[const.VARIANT_ESP32P4]), + cv.only_with_esp_idf, + ) + + +def _config_schema(config): + config = cv.Schema( + { + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=cv.ALLOW_EXTRA, + )(config) + return model_schema(config)(config) + + +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 + return config + + +CONFIG_SCHEMA = _config_schema +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + model = MODELS[config[CONF_MODEL].upper()] + color_depth = COLOR_DEPTHS[get_color_depth(config)] + pixel_mode = int(config[CONF_PIXEL_MODE].removesuffix("bit")) + width, height, _offset_width, _offset_height = model.get_dimensions(config) + var = cg.new_Pvariable(config[CONF_ID], width, height, color_depth, pixel_mode) + + sequence, madctl = model.get_sequence(config) + cg.add(var.set_model(config[CONF_MODEL])) + cg.add(var.set_init_sequence(sequence)) + cg.add(var.set_madctl(madctl)) + cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) + cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH])) + cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH])) + cg.add(var.set_hsync_front_porch(config[CONF_HSYNC_FRONT_PORCH])) + cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH])) + cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH])) + cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) + cg.add(var.set_pclk_frequency(int(config[CONF_PCLK_FREQUENCY] / 1e6))) + cg.add(var.set_lanes(int(config[CONF_LANES]))) + cg.add(var.set_lane_bit_rate(int(config[CONF_LANE_BIT_RATE] / 1e6))) + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(reset)) + if enable_pin := config.get(CONF_ENABLE_PIN): + enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] + cg.add(var.set_enable_pins(enable)) + + if model.rotation_as_transform(config): + config[CONF_ROTATION] = 0 + await display.register_display(var, config) + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp new file mode 100644 index 0000000000..fbe251de41 --- /dev/null +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -0,0 +1,379 @@ +#ifdef USE_ESP32_VARIANT_ESP32P4 +#include +#include "mipi_dsi.h" + +namespace esphome { +namespace mipi_dsi { + +static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) { + auto *sem = static_cast(user_ctx); + BaseType_t need_yield = pdFALSE; + xSemaphoreGiveFromISR(sem, &need_yield); + return (need_yield == pdTRUE); +} +void MIPI_DSI::setup() { + ESP_LOGCONFIG(TAG, "Running Setup"); + + if (!this->enable_pins_.empty()) { + for (auto *pin : this->enable_pins_) { + pin->setup(); + pin->digital_write(true); + } + delay(10); + } + + esp_lcd_dsi_bus_config_t bus_config = { + .bus_id = 0, // index from 0, specify the DSI host to use + .num_data_lanes = + this->lanes_, // Number of data lanes to use, can't set a value that exceeds the chip's capability + .phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, // Clock source for the DPHY + .lane_bit_rate_mbps = this->lane_bit_rate_, // Bit rate of the data lanes, in Mbps + }; + auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_); + if (err != ESP_OK) { + this->smark_failed("lcd_new_dsi_bus failed", err); + return; + } + esp_lcd_dbi_io_config_t dbi_config = { + .virtual_channel = 0, + .lcd_cmd_bits = 8, // according to the LCD spec + .lcd_param_bits = 8, // according to the LCD spec + }; + err = esp_lcd_new_panel_io_dbi(this->bus_handle_, &dbi_config, &this->io_handle_); + if (err != ESP_OK) { + this->smark_failed("new_panel_io_dbi failed", err); + return; + } + auto pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + if (this->color_depth_ == display::COLOR_BITNESS_888) { + pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB888; + } + esp_lcd_dpi_panel_config_t dpi_config = {.virtual_channel = 0, + .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, + .dpi_clock_freq_mhz = this->pclk_frequency_, + .pixel_format = pixel_format, + .num_fbs = 1, // number of frame buffers to allocate + .video_timing = + { + .h_size = this->width_, + .v_size = this->height_, + .hsync_pulse_width = this->hsync_pulse_width_, + .hsync_back_porch = this->hsync_back_porch_, + .hsync_front_porch = this->hsync_front_porch_, + .vsync_pulse_width = this->vsync_pulse_width_, + .vsync_back_porch = this->vsync_back_porch_, + .vsync_front_porch = this->vsync_front_porch_, + }, + .flags = { + .use_dma2d = true, + }}; + err = esp_lcd_new_panel_dpi(this->bus_handle_, &dpi_config, &this->handle_); + if (err != ESP_OK) { + this->smark_failed("esp_lcd_new_panel_dpi failed", err); + return; + } + 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); + } else { + esp_lcd_panel_io_tx_param(this->io_handle_, SW_RESET_CMD, nullptr, 0); + } + // need to know when the display is ready for SLPOUT command - will be 120ms after reset + auto when = millis() + 120; + err = esp_lcd_panel_init(this->handle_); + if (err != ESP_OK) { + this->smark_failed("esp_lcd_init failed", err); + return; + } + size_t index = 0; + auto &vec = this->init_sequence_; + while (index != vec.size()) { + if (vec.size() - index < 2) { + this->mark_failed("Malformed init sequence"); + return; + } + uint8_t cmd = vec[index++]; + uint8_t x = vec[index++]; + if (x == DELAY_FLAG) { + ESP_LOGD(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + uint8_t num_args = x & 0x7F; + if (vec.size() - index < num_args) { + this->mark_failed("Malformed init sequence"); + return; + } + if (cmd == SLEEP_OUT) { + // are we ready, boots? + int duration = when - millis(); + if (duration > 0) { + delay(duration); + } + } + const auto *ptr = vec.data() + index; + ESP_LOGVV(TAG, "Command %02X, length %d, byte(s) %s", cmd, num_args, + format_hex_pretty(ptr, num_args, '.', false).c_str()); + err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args); + if (err != ESP_OK) { + this->smark_failed("lcd_panel_io_tx_param failed", err); + return; + } + index += num_args; + if (cmd == SLEEP_OUT) + delay(10); + } + } + this->io_lock_ = xSemaphoreCreateBinary(); + esp_lcd_dpi_panel_event_callbacks_t cbs = { + .on_color_trans_done = notify_refresh_ready, + }; + + err = (esp_lcd_dpi_panel_register_event_callbacks(this->handle_, &cbs, this->io_lock_)); + if (err != ESP_OK) { + this->smark_failed("Failed to register callbacks", err); + return; + } + + ESP_LOGCONFIG(TAG, "MIPI DSI setup complete"); +} + +void MIPI_DSI::update() { + if (this->auto_clear_enabled_) { + this->clear(); + } + if (this->show_test_card_) { + this->test_card(); + } else if (this->page_ != nullptr) { + this->page_->get_writer()(*this); + } else if (this->writer_.has_value()) { + (*this->writer_)(*this); + } else { + this->stop_poller(); + } + if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) + return; + ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_); + int w = this->x_high_ - this->x_low_ + 1; + int h = this->y_high_ - this->y_low_ + 1; + this->write_to_display_(this->x_low_, this->y_low_, w, h, 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 MIPI_DSI::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { + if (w <= 0 || h <= 0) + return; + // if color mapping is required, pass the buck. + // note that endianness is not considered here - it is assumed to match! + if (bitness != this->color_depth_) { + display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, + x_pad); + } + this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); +} + +void MIPI_DSI::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad) { + esp_err_t err = ESP_OK; + auto bytes_per_pixel = 3 - this->color_depth_; + auto stride = (x_offset + w + x_pad) * bytes_per_pixel; + ptr += y_offset * stride + x_offset * bytes_per_pixel; // skip to the first pixel + // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. + if (x_offset == 0 && x_pad == 0) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y_start, x_start + w, y_start + h, ptr); + xSemaphoreTake(this->io_lock_, portMAX_DELAY); + + } else { + // draw line by line + for (int y = 0; y != h; y++) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y + y_start, x_start + w, y + y_start + 1, ptr); + if (err != ESP_OK) + break; + ptr += stride; // next line + xSemaphoreTake(this->io_lock_, portMAX_DELAY); + } + } + if (err != ESP_OK) + ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); +} + +bool MIPI_DSI::check_buffer_() { + if (this->is_failed()) + return false; + if (this->buffer_ != nullptr) + return true; + // this is dependent on the enum values. + auto bytes_per_pixel = 3 - this->color_depth_; + RAMAllocator allocator; + this->buffer_ = allocator.allocate(this->height_ * this->width_ * bytes_per_pixel); + if (this->buffer_ == nullptr) { + this->mark_failed("Could not allocate buffer for display!"); + return false; + } + return true; +} + +void MIPI_DSI::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y)) + return; + + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + break; + case display::DISPLAY_ROTATION_90_DEGREES: + std::swap(x, y); + x = this->width_ - x - 1; + break; + case display::DISPLAY_ROTATION_180_DEGREES: + x = this->width_ - x - 1; + y = this->height_ - y - 1; + break; + case display::DISPLAY_ROTATION_270_DEGREES: + std::swap(x, y); + y = this->height_ - y - 1; + break; + } + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { + return; + } + auto pixel = convert_big_endian(display::ColorUtil::color_to_565(color)); + if (!this->check_buffer_()) + return; + size_t pos = (y * this->width_) + x; + switch (this->color_depth_) { + 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 = lo_byte | (hi_byte << 8); // little endian + if (ptr_16[pos] == new_color) + return; + ptr_16[pos] = new_color; + break; + } + case display::COLOR_BITNESS_888: + if (this->color_mode_ == display::COLOR_ORDER_BGR) { + this->buffer_[pos * 3] = color.b; + this->buffer_[pos * 3 + 1] = color.g; + this->buffer_[pos * 3 + 2] = color.r; + } else { + this->buffer_[pos * 3] = color.r; + this->buffer_[pos * 3 + 1] = color.g; + this->buffer_[pos * 3 + 2] = color.b; + } + break; + case display::COLOR_BITNESS_332: + break; + } + // 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 MIPI_DSI::fill(Color color) { + if (!this->check_buffer_()) + return; + switch (this->color_depth_) { + 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 = lo_byte | (hi_byte << 8); // little endian + std::fill_n(ptr_16, this->width_ * this->height_, new_color); + break; + } + + case display::COLOR_BITNESS_888: + if (this->color_mode_ == display::COLOR_ORDER_BGR) { + for (size_t i = 0; i != this->width_ * this->height_; i++) { + this->buffer_[i * 3 + 0] = color.b; + this->buffer_[i * 3 + 1] = color.g; + this->buffer_[i * 3 + 2] = color.r; + } + } else { + for (size_t i = 0; i != this->width_ * this->height_; i++) { + this->buffer_[i * 3 + 0] = color.r; + this->buffer_[i * 3 + 1] = color.g; + this->buffer_[i * 3 + 2] = color.b; + } + } + + default: + break; + } +} + +int MIPI_DSI::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + default: + return this->get_width_internal(); + } +} + +int MIPI_DSI::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + default: + return this->get_width_internal(); + } +} + +static const uint8_t PIXEL_MODES[] = {0, 16, 18, 24}; + +void MIPI_DSI::dump_config() { + ESP_LOGCONFIG(TAG, + "MIPI_DSI RGB LCD" + "\n Model: %s" + "\n Width: %u" + "\n Height: %u" + "\n Mirror X: %s" + "\n Mirror Y: %s" + "\n Swap X/Y: %s" + "\n Rotation: %d degrees" + "\n DSI Lanes: %u" + "\n Lane Bit Rate: %uMbps" + "\n HSync Pulse Width: %u" + "\n HSync Back Porch: %u" + "\n HSync Front Porch: %u" + "\n VSync Pulse Width: %u" + "\n VSync Back Porch: %u" + "\n VSync Front Porch: %u" + "\n Buffer Color Depth: %d bit" + "\n Display Pixel Mode: %d bit" + "\n Color Order: %s" + "\n Invert Colors: %s" + "\n Pixel Clock: %dMHz", + this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), + YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_, + this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_, + this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, this->vsync_front_porch_, + (3 - this->color_depth_) * 8, this->pixel_mode_, this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", + YESNO(this->invert_colors_), this->pclk_frequency_); + LOG_PIN(" Reset Pin ", this->reset_pin_); +} +} // namespace mipi_dsi +} // namespace esphome +#endif // USE_ESP32_VARIANT_ESP32P4 diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h new file mode 100644 index 0000000000..ce8a2a2236 --- /dev/null +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -0,0 +1,123 @@ +// +// Created by Clyde Stubbs on 29/10/2023. +// +#pragma once + +// only applicable on ESP32-P4 +#ifdef USE_ESP32_VARIANT_ESP32P4 +#include "esphome/core/component.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/gpio.h" + +#include "esphome/components/display/display.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_io.h" + +#include "esp_lcd_mipi_dsi.h" + +namespace esphome { +namespace mipi_dsi { + +constexpr static const char *const TAG = "display.mipi_dsi"; +const uint8_t SW_RESET_CMD = 0x01; +const uint8_t SLEEP_OUT = 0x11; +const uint8_t SDIR_CMD = 0xC7; +const uint8_t MADCTL_CMD = 0x36; +const uint8_t INVERT_OFF = 0x20; +const uint8_t INVERT_ON = 0x21; +const uint8_t DISPLAY_ON = 0x29; +const uint8_t CMD2_BKSEL = 0xFF; +const uint8_t DELAY_FLAG = 0xFF; +const uint8_t MADCTL_BGR = 0x08; +const uint8_t MADCTL_MX = 0x40; +const uint8_t MADCTL_MY = 0x80; +const uint8_t MADCTL_MV = 0x20; // row/column swap +const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally +const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically + +class MIPI_DSI : public display::Display { + public: + MIPI_DSI(size_t width, size_t height, display::ColorBitness color_depth, uint8_t pixel_mode) + : width_(width), height_(height), color_depth_(color_depth), pixel_mode_(pixel_mode) {} + display::ColorOrder get_color_mode() { return this->color_mode_; } + void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; } + void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; } + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + + 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_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } + void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; } + void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; } + void set_vsync_pulse_width(uint16_t vsync_pulse_width) { this->vsync_pulse_width_ = vsync_pulse_width; } + void set_vsync_back_porch(uint16_t vsync_back_porch) { this->vsync_back_porch_ = vsync_back_porch; } + void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; } + void set_init_sequence(const std::vector &init_sequence) { this->init_sequence_ = init_sequence; } + void set_model(const char *model) { this->model_ = model; } + void set_lane_bit_rate(uint16_t lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; } + void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } + void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } + + void smark_failed(const char *message, esp_err_t err) { + auto str = str_sprintf("Setup failed: %s: %s", message, esp_err_to_name(err)); + this->mark_failed(str.c_str()); + } + + void update() override; + + void setup() 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 draw_pixel_at(int x, int y, Color color) override; + void fill(Color color) override; + int get_width() override; + int get_height() override; + + void dump_config() override; + + protected: + void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad); + bool check_buffer_(); + GPIOPin *reset_pin_{nullptr}; + std::vector enable_pins_{}; + size_t width_{}; + size_t height_{}; + uint8_t madctl_{}; + uint16_t hsync_pulse_width_ = 10; + uint16_t hsync_back_porch_ = 10; + uint16_t hsync_front_porch_ = 20; + uint16_t vsync_pulse_width_ = 10; + uint16_t vsync_back_porch_ = 10; + uint16_t vsync_front_porch_ = 10; + const char *model_{"Unknown"}; + std::vector init_sequence_{}; + uint16_t pclk_frequency_ = 16; // in MHz + uint16_t lane_bit_rate_{1500}; // in Mbps + uint8_t lanes_{2}; // 1, 2, 3 or 4 lanes + + bool invert_colors_{}; + display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; + display::ColorBitness color_depth_; + uint8_t pixel_mode_{}; + + esp_lcd_panel_handle_t handle_{}; + esp_lcd_dsi_bus_handle_t bus_handle_{}; + esp_lcd_panel_io_handle_t io_handle_{}; + SemaphoreHandle_t io_lock_{}; + uint8_t *buffer_{nullptr}; + uint16_t x_low_{1}; + uint16_t y_low_{1}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; +}; + +} // namespace mipi_dsi +} // namespace esphome +#endif diff --git a/esphome/components/mipi_dsi/models/__init__.py b/esphome/components/mipi_dsi/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mipi_dsi/models/guition.py b/esphome/components/mipi_dsi/models/guition.py new file mode 100644 index 0000000000..fd3fbf6160 --- /dev/null +++ b/esphome/components/mipi_dsi/models/guition.py @@ -0,0 +1,38 @@ +from esphome.components.mipi import DriverChip +import esphome.config_validation as cv + +# fmt: off +DriverChip( + "JC1060P470", + width=1024, + height=600, + hsync_back_porch=160, + hsync_pulse_width=40, + hsync_front_porch=160, + vsync_back_porch=23, + vsync_pulse_width=10, + vsync_front_porch=12, + pclk_frequency="54MHz", + lane_bit_rate="750Mbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + reset_pin=27, + initsequence=[ + (0x30, 0x00), (0xF7, 0x49, 0x61, 0x02, 0x00), (0x30, 0x01), (0x04, 0x0C), (0x05, 0x00), (0x06, 0x00), + (0x0B, 0x11), (0x17, 0x00), (0x20, 0x04), (0x1F, 0x05), (0x23, 0x00), (0x25, 0x19), (0x28, 0x18), (0x29, 0x04), (0x2A, 0x01), + (0x2B, 0x04), (0x2C, 0x01), (0x30, 0x02), (0x01, 0x22), (0x03, 0x12), (0x04, 0x00), (0x05, 0x64), (0x0A, 0x08), + (0x0B, 0x0A, 0x1A, 0x0B, 0x0D, 0x0D, 0x11, 0x10, 0x06, 0x08, 0x1F, 0x1D), + (0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D), + (0x0D, 0x16, 0x1B, 0x0B, 0x0D, 0x0D, 0x11, 0x10, 0x07, 0x09, 0x1E, 0x1C), + (0x0E, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D), + (0x0F, 0x16, 0x1B, 0x0D, 0x0B, 0x0D, 0x11, 0x10, 0x1C, 0x1E, 0x09, 0x07), + (0x10, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D), + (0x11, 0x0A, 0x1A, 0x0D, 0x0B, 0x0D, 0x11, 0x10, 0x1D, 0x1F, 0x08, 0x06), + (0x12, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D), + (0x14, 0x00, 0x00, 0x11, 0x11), (0x18, 0x99), (0x30, 0x06), + (0x12, 0x36, 0x2C, 0x2E, 0x3C, 0x38, 0x35, 0x35, 0x32, 0x2E, 0x1D, 0x2B, 0x21, 0x16, 0x29,), + (0x13, 0x36, 0x2C, 0x2E, 0x3C, 0x38, 0x35, 0x35, 0x32, 0x2E, 0x1D, 0x2B, 0x21, 0x16, 0x29,), + (0x30, 0x0A), (0x02, 0x4F), (0x0B, 0x40), (0x12, 0x3E), (0x13, 0x78), (0x30, 0x0D), (0x0D, 0x04), + (0x10, 0x0C), (0x11, 0x0C), (0x12, 0x0C), (0x13, 0x0C), (0x30, 0x00), + ], +) diff --git a/esphome/components/mipi_dsi/models/m5stack.py b/esphome/components/mipi_dsi/models/m5stack.py new file mode 100644 index 0000000000..6055c77f8f --- /dev/null +++ b/esphome/components/mipi_dsi/models/m5stack.py @@ -0,0 +1,57 @@ +from esphome.components.mipi import DriverChip +import esphome.config_validation as cv + +# fmt: off +DriverChip( + "M5STACK-TAB5", + height=1280, + width=720, + hsync_back_porch=140, + hsync_pulse_width=40, + hsync_front_porch=40, + vsync_back_porch=20, + vsync_pulse_width=4, + vsync_front_porch=20, + pclk_frequency="60MHz", + lane_bit_rate="730Mbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + initsequence=[ + (0xFF, 0x98, 0x81, 0x01), # Select Page 1 + (0xB7, 0x03), # Pad control - 2 lane + (0xFF, 0x98, 0x81, 0x00), # Select Page 0 + # CMD_Page 3 + (0xFF, 0x98, 0x81, 0x03), # Select Page 3 + (0x01, 0x00), (0x02, 0x00), (0x03, 0x73), (0x04, 0x00), (0x05, 0x00), (0x06, 0x08), (0x07, 0x00), (0x08, 0x00), + (0x09, 0x1B), (0x0A, 0x01), (0x0B, 0x01), (0x0C, 0x0D), (0x0D, 0x01), (0x0E, 0x01), (0x0F, 0x26), (0x10, 0x26), + (0x11, 0x00), (0x12, 0x00), (0x13, 0x02), (0x14, 0x00), (0x15, 0x00), (0x16, 0x00), (0x17, 0x00), (0x18, 0x00), + (0x19, 0x00), (0x1A, 0x00), (0x1B, 0x00), (0x1C, 0x00), (0x1D, 0x00), (0x1E, 0x40), (0x1F, 0x00), (0x20, 0x06), + (0x21, 0x01), (0x22, 0x00), (0x23, 0x00), (0x24, 0x00), (0x25, 0x00), (0x26, 0x00), (0x27, 0x00), (0x28, 0x33), + (0x29, 0x03), (0x2A, 0x00), (0x2B, 0x00), (0x2C, 0x00), (0x2D, 0x00), (0x2E, 0x00), (0x2F, 0x00), (0x30, 0x00), + (0x31, 0x00), (0x32, 0x00), (0x33, 0x00), (0x34, 0x00), (0x35, 0x00), (0x36, 0x00), (0x37, 0x00), (0x38, 0x00), + (0x39, 0x00), (0x3A, 0x00), (0x3B, 0x00), (0x3C, 0x00), (0x3D, 0x00), (0x3E, 0x00), (0x3F, 0x00), (0x40, 0x00), + (0x41, 0x00), (0x42, 0x00), (0x43, 0x00), (0x44, 0x00), (0x50, 0x01), (0x51, 0x23), (0x52, 0x45), (0x53, 0x67), + (0x54, 0x89), (0x55, 0xAB), (0x56, 0x01), (0x57, 0x23), (0x58, 0x45), (0x59, 0x67), (0x5A, 0x89), (0x5B, 0xAB), + (0x5C, 0xCD), (0x5D, 0xEF), (0x5E, 0x11), (0x5F, 0x02), (0x60, 0x00), (0x61, 0x07), (0x62, 0x06), (0x63, 0x0E), + (0x64, 0x0F), (0x65, 0x0C), (0x66, 0x0D), (0x67, 0x02), (0x68, 0x02), (0x69, 0x02), (0x6A, 0x02), (0x6B, 0x02), + (0x6C, 0x02), (0x6D, 0x02), (0x6E, 0x02), (0x6F, 0x02), (0x70, 0x02), (0x71, 0x02), (0x72, 0x02), (0x73, 0x05), + (0x74, 0x01), (0x75, 0x02), (0x76, 0x00), (0x77, 0x07), (0x78, 0x06), (0x79, 0x0E), (0x7A, 0x0F), (0x7B, 0x0C), + (0x7C, 0x0D), (0x7D, 0x02), (0x7E, 0x02), (0x7F, 0x02), (0x80, 0x02), (0x81, 0x02), (0x82, 0x02), (0x83, 0x02), + (0x84, 0x02), (0x85, 0x02), (0x86, 0x02), (0x87, 0x02), (0x88, 0x02), (0x89, 0x05), (0x8A, 0x01), + (0xFF, 0x98, 0x81, 0x04), # Select Page 4 + (0x38, 0x01), (0x39, 0x00), (0x6C, 0x15), (0x6E, 0x1A), (0x6F, 0x25), (0x3A, 0xA4), (0x8D, 0x20), (0x87, 0xBA), (0x3B, 0x98), + (0xFF, 0x98, 0x81, 0x01), # Select Page 1 + (0x22, 0x0A), (0x31, 0x00), (0x50, 0x6B), (0x51, 0x66), (0x53, 0x73), (0x55, 0x8B), (0x60, 0x1B), (0x61, 0x01), (0x62, 0x0C), (0x63, 0x00), + # Gamma P + (0xA0, 0x00), (0xA1, 0x15), (0xA2, 0x1F), (0xA3, 0x13), (0xA4, 0x11), (0xA5, 0x21), (0xA6, 0x17), (0xA7, 0x1B), + (0xA8, 0x6B), (0xA9, 0x1E), (0xAA, 0x2B), (0xAB, 0x5D), (0xAC, 0x19), (0xAD, 0x14), (0xAE, 0x4B), (0xAF, 0x1D), + (0xB0, 0x27), (0xB1, 0x49), (0xB2, 0x5D), (0xB3, 0x39), + # Gamma N + (0xC0, 0x00), (0xC1, 0x01), (0xC2, 0x0C), (0xC3, 0x11), (0xC4, 0x15), (0xC5, 0x28), (0xC6, 0x1B), (0xC7, 0x1C), + (0xC8, 0x62), (0xC9, 0x1C), (0xCA, 0x29), (0xCB, 0x60), (0xCC, 0x16), (0xCD, 0x17), (0xCE, 0x4A), (0xCF, 0x23), + (0xD0, 0x24), (0xD1, 0x4F), (0xD2, 0x5F), (0xD3, 0x39), + # CMD_Page 0 + (0xFF, 0x98, 0x81, 0x00), # Select Page 0 + (0x35,), (0xFE,), + ], +) diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py new file mode 100644 index 0000000000..7cfd6f1645 --- /dev/null +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -0,0 +1,105 @@ +from esphome.components.mipi import DriverChip +import esphome.config_validation as cv + +# fmt: off +DriverChip( + "WAVESHARE-P4-NANO-10.1", + height=1280, + width=800, + hsync_back_porch=20, + hsync_pulse_width=20, + hsync_front_porch=40, + vsync_back_porch=12, + vsync_pulse_width=4, + vsync_front_porch=30, + pclk_frequency="80MHz", + lane_bit_rate="1.5Gbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + initsequence=[ + (0xE0, 0x00), # select userpage + (0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8), + (0x80, 0x01), # Select number of lanes (2) + (0xE0, 0x01), # select page 1 + (0x00, 0x00), (0x01, 0x38), (0x03, 0x10), (0x04, 0x38), (0x0C, 0x74), (0x17, 0x00), (0x18, 0xAF), (0x19, 0x00), + (0x1A, 0x00), (0x1B, 0xAF), (0x1C, 0x00), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x00), (0x3C, 0x78), + (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0x7F), (0x40, 0x06), (0x41, 0xA0), (0x42, 0x81), (0x43, 0x1E), (0x44, 0x0D), + (0x45, 0x28), (0x55, 0x02), (0x57, 0x69), (0x59, 0x0A), (0x5A, 0x2A), (0x5B, 0x17), (0x5D, 0x7F), (0x5E, 0x6A), + (0x5F, 0x5B), (0x60, 0x4F), (0x61, 0x4A), (0x62, 0x3D), (0x63, 0x41), (0x64, 0x2A), (0x65, 0x44), (0x66, 0x43), + (0x67, 0x44), (0x68, 0x62), (0x69, 0x52), (0x6A, 0x59), (0x6B, 0x4C), (0x6C, 0x48), (0x6D, 0x3A), (0x6E, 0x26), + (0x6F, 0x00), (0x70, 0x7F), (0x71, 0x6A), (0x72, 0x5B), (0x73, 0x4F), (0x74, 0x4A), (0x75, 0x3D), (0x76, 0x41), + (0x77, 0x2A), (0x78, 0x44), (0x79, 0x43), (0x7A, 0x44), (0x7B, 0x62), (0x7C, 0x52), (0x7D, 0x59), (0x7E, 0x4C), + (0x7F, 0x48), (0x80, 0x3A), (0x81, 0x26), (0x82, 0x00), + (0xE0, 0x02), # select page 2 + (0x00, 0x42), (0x01, 0x42), (0x02, 0x40), (0x03, 0x40), (0x04, 0x5E), (0x05, 0x5E), (0x06, 0x5F), (0x07, 0x5F), + (0x08, 0x5F), (0x09, 0x57), (0x0A, 0x57), (0x0B, 0x77), (0x0C, 0x77), (0x0D, 0x47), (0x0E, 0x47), (0x0F, 0x45), + (0x10, 0x45), (0x11, 0x4B), (0x12, 0x4B), (0x13, 0x49), (0x14, 0x49), (0x15, 0x5F), (0x16, 0x41), (0x17, 0x41), + (0x18, 0x40), (0x19, 0x40), (0x1A, 0x5E), (0x1B, 0x5E), (0x1C, 0x5F), (0x1D, 0x5F), (0x1E, 0x5F), (0x1F, 0x57), + (0x20, 0x57), (0x21, 0x77), (0x22, 0x77), (0x23, 0x46), (0x24, 0x46), (0x25, 0x44), (0x26, 0x44), (0x27, 0x4A), + (0x28, 0x4A), (0x29, 0x48), (0x2A, 0x48), (0x2B, 0x5F), (0x2C, 0x01), (0x2D, 0x01), (0x2E, 0x00), (0x2F, 0x00), + (0x30, 0x1F), (0x31, 0x1F), (0x32, 0x1E), (0x33, 0x1E), (0x34, 0x1F), (0x35, 0x17), (0x36, 0x17), (0x37, 0x37), + (0x38, 0x37), (0x39, 0x08), (0x3A, 0x08), (0x3B, 0x0A), (0x3C, 0x0A), (0x3D, 0x04), (0x3E, 0x04), (0x3F, 0x06), + (0x40, 0x06), (0x41, 0x1F), (0x42, 0x02), (0x43, 0x02), (0x44, 0x00), (0x45, 0x00), (0x46, 0x1F), (0x47, 0x1F), + (0x48, 0x1E), (0x49, 0x1E), (0x4A, 0x1F), (0x4B, 0x17), (0x4C, 0x17), (0x4D, 0x37), (0x4E, 0x37), (0x4F, 0x09), + (0x50, 0x09), (0x51, 0x0B), (0x52, 0x0B), (0x53, 0x05), (0x54, 0x05), (0x55, 0x07), (0x56, 0x07), (0x57, 0x1F), + (0x58, 0x40), (0x5B, 0x30), (0x5C, 0x00), (0x5D, 0x34), (0x5E, 0x05), (0x5F, 0x02), (0x63, 0x00), (0x64, 0x6A), + (0x67, 0x73), (0x68, 0x07), (0x69, 0x08), (0x6A, 0x6A), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x00), (0x6E, 0x00), + (0x6F, 0x88), (0x75, 0xFF), (0x77, 0xDD), (0x78, 0x2C), (0x79, 0x15), (0x7A, 0x17), (0x7D, 0x14), (0x7E, 0x82), + (0xE0, 0x04), # select page 4 + (0x00, 0x0E), (0x02, 0xB3), (0x09, 0x61), (0x0E, 0x48), (0x37, 0x58), (0x2B, 0x0F), + (0xE0, 0x00), # Select userpage + (0xE6, 0x02), (0xE7, 0x0C), + ], +) + +DriverChip( + "WAVESHARE-P4-86-PANEL", + height=720, + width=720, + hsync_back_porch=80, + hsync_pulse_width=20, + hsync_front_porch=80, + vsync_back_porch=12, + vsync_pulse_width=4, + vsync_front_porch=30, + pclk_frequency="46MHz", + lane_bit_rate="1Gbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + reset_pin=27, + initsequence=[ + (0xB9, 0xF1, 0x12, 0x83), + ( + 0xBA, 0x31, 0x81, 0x05, 0xF9, 0x0E, 0x0E, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00, + 0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37, + ), + (0xB8, 0x25, 0x22, 0xF0, 0x63), + (0xBF, 0x02, 0x11, 0x00), + (0xB3, 0x10, 0x10, 0x28, 0x28, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00), + (0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00), + (0xBC, 0x46), (0xCC, 0x0B), (0xB4, 0x80), (0xB2, 0x3C, 0x12, 0x30), + (0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x03, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10,), + (0xC1, 0x36, 0x00, 0x32, 0x32, 0x77, 0xF1, 0xCC, 0xCC, 0x77, 0x77, 0x33, 0x33), + (0xB5, 0x0A, 0x0A), + (0xB6, 0xB2, 0xB2), + ( + 0xE9, 0xC8, 0x10, 0x0A, 0x10, 0x0F, 0xA1, 0x80, 0x12, 0x31, 0x23, 0x47, 0x86, 0xA1, 0x80, + 0x47, 0x08, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x48, + 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x48, 0x13, 0x8B, 0xAF, 0x57, + 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ), + ( + 0xEA, 0x96, 0x12, 0x01, 0x01, 0x01, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x31, + 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x4F, 0x20, 0x8B, 0xA8, 0x20, 0x64, + 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xA1, 0x80, 0x00, 0x00, + 0x00, 0x00, + ), + ( + 0xE0, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13, 0x15, 0x14, + 0x15, 0x10, 0x17, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13, + 0x15, 0x14, 0x15, 0x10, 0x17, + ), + ], +) diff --git a/esphome/components/mipi_rgb/models/lilygo.py b/esphome/components/mipi_rgb/models/lilygo.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mipi_spi/__init__.py b/esphome/components/mipi_spi/__init__.py index 46b0206a1f..f0f02aedd8 100644 --- a/esphome/components/mipi_spi/__init__.py +++ b/esphome/components/mipi_spi/__init__.py @@ -2,14 +2,5 @@ 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" -CONF_NATIVE_HEIGHT = "native_height" - -MODE_RGB = "RGB" -MODE_BGR = "BGR" diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 061257e859..e891e2daad 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -1,20 +1,43 @@ +import importlib import logging +import pkgutil 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.mipi import ( + CONF_PIXEL_MODE, + CONF_USE_AXIS_FLIPS, + MADCTL, + MODE_BGR, + MODE_RGB, + PIXFMT, + DriverChip, + dimension_schema, + get_color_depth, + map_sequence, + power_of_two, + requires_buffer, +) +from esphome.components.psram import DOMAIN as PSRAM_DOMAIN 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, CONF_DC_PIN, CONF_DIMENSIONS, CONF_ENABLE_PIN, - CONF_HEIGHT, CONF_ID, CONF_INIT_SEQUENCE, CONF_INVERT_COLORS, @@ -22,177 +45,135 @@ from esphome.const import ( CONF_MIRROR_X, CONF_MIRROR_Y, CONF_MODEL, - CONF_OFFSET_HEIGHT, - CONF_OFFSET_WIDTH, CONF_RESET_PIN, CONF_ROTATION, CONF_SWAP_XY, CONF_TRANSFORM, CONF_WIDTH, ) -from esphome.core import TimePeriod +from esphome.core import CORE +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, - CONF_SPI_16, - CONF_USE_AXIS_FLIPS, - DOMAIN, - MODE_BGR, - MODE_RGB, -) -from .models import ( - DELAY_FLAG, - MADCTL_BGR, - MADCTL_MV, - MADCTL_MX, - MADCTL_MY, - MADCTL_XFLIP, - MADCTL_YFLIP, - DriverChip, - amoled, - cyd, - ili, - jc, - lanbon, - lilygo, - waveshare, -) -from .models.commands import BRIGHTNESS, DISPON, INVOFF, INVON, MADCTL, PIXFMT, SLPOUT +from . import CONF_BUS_MODE, CONF_SPI_16, DOMAIN, models 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): - pass +# Import all models dynamically from the models package +for module_info in pkgutil.iter_modules(models.__path__): + importlib.import_module(f".models.{module_info.name}", package=__package__) -PixelMode = mipi_spi_ns.enum("PixelMode") +MODELS = DriverChip.get_models() -PIXEL_MODE_18BIT = "18bit" -PIXEL_MODE_16BIT = "16bit" -PIXEL_MODES = { - PIXEL_MODE_16BIT: 0x55, - PIXEL_MODE_18BIT: 0x66, +DISPLAY_18BIT = "18bit" +DISPLAY_16BIT = "16bit" + +DISPLAY_PIXEL_MODES = { + DISPLAY_16BIT: (0x55, PixelMode.PIXEL_MODE_16), + DISPLAY_18BIT: (0x66, PixelMode.PIXEL_MODE_18), } -def validate_dimension(rounding): +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 + """ + model = MODELS[config[CONF_MODEL]] + frac = config.get(CONF_BUFFER_SIZE) + if frac is None or frac > 0.75: + return 1 + height, _width, _offset_width, _offset_height = model.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 swap_xy_schema(model): + uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED + def validator(value): - value = cv.positive_int(value) - if value % rounding != 0: - raise cv.Invalid(f"Dimensions and offsets must be divisible by {rounding}") - return value + if value: + raise cv.Invalid("Axis swapping not supported by this model") + return cv.boolean(value) - return validator + if uses_swap: + return {cv.Required(CONF_SWAP_XY): cv.boolean} + return {cv.Optional(CONF_SWAP_XY, default=False): validator} -def map_sequence(value): - """ - The format is a repeated sequence of [CMD, ] where is s a sequence of bytes. The length is inferred - from the length of the sequence and should not be explicit. - A delay can be inserted by specifying "- delay N" where N is in ms - """ - if isinstance(value, str) and value.lower().startswith("delay "): - value = value.lower()[6:] - delay = cv.All( - cv.positive_time_period_milliseconds, - cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)), - )(value) - return DELAY_FLAG, delay.total_milliseconds - if isinstance(value, int): - return (value,) - value = cv.All(cv.ensure_list(cv.int_range(0, 255)), cv.Length(1, 254))(value) - return tuple(value) - - -def power_of_two(value): - value = cv.int_range(1, 128)(value) - if value & (value - 1) != 0: - raise cv.Invalid("value must be a power of two") - return value - - -def dimension_schema(rounding): - return cv.Any( - cv.dimensions, - cv.Schema( - { - cv.Required(CONF_WIDTH): validate_dimension(rounding), - cv.Required(CONF_HEIGHT): validate_dimension(rounding), - cv.Optional(CONF_OFFSET_HEIGHT, default=0): validate_dimension( - rounding - ), - cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding), - } - ), - ) - - -def model_schema(bus_mode, model: DriverChip, swapsies: bool): +def model_schema(config): + model = MODELS[config[CONF_MODEL]] + bus_mode = config[CONF_BUS_MODE] transform = cv.Schema( { cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean, + **swap_xy_schema(model), } ) - 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 +201,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 +215,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 +235,13 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool): return schema -def rotation_as_transform(model, config): +def customise_schema(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. + 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 """ - rotation = config.get(CONF_ROTATION, 0) - return rotation and ( - model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 - ) - - -def config_schema(config): # First get the model and bus mode config = cv.Schema( { @@ -279,7 +250,7 @@ def config_schema(config): extra=ALLOW_EXTRA, )(config) model = MODELS[config[CONF_MODEL]] - bus_modes = model.modes + bus_modes = (TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL) config = cv.Schema( { model.option(CONF_BUS_MODE, TYPE_SINGLE): cv.one_of(*bus_modes, lower=True), @@ -287,30 +258,81 @@ 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) + bus_mode = config[CONF_BUS_MODE] + 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 _final_validate(config): + global_config = full_config.get() + model = MODELS[config[CONF_MODEL]] + + 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_DOMAIN 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 = model.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 = model.rotation_as_transform(config) transform = config.get( CONF_TRANSFORM, { @@ -336,123 +358,64 @@ def get_transform(model, config): return transform -def get_sequence(model, config): +def get_instance(config): """ - Create the init sequence for the display. - Use the default sequence from the model, if any, and append any custom sequence provided in the config. - Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence - Pixel format, color order, and orientation will be set. + Get the type of MipiSpi instance to create based on the configuration, + and the template arguments. + :param config: + :return: type, template arguments """ - sequence = list(model.initsequence) - custom_sequence = config.get(CONF_INIT_SEQUENCE, []) - sequence.extend(custom_sequence) - # Ensure each command is a tuple - 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)) - # 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) - if transform.get(CONF_TRANSFORM): - LOGGER.info("Using hardware transform to implement rotation") - if transform.get(CONF_MIRROR_X): - madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX - if transform.get(CONF_MIRROR_Y): - madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY - if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined - madctl |= MADCTL_MV - if config[CONF_COLOR_ORDER] == MODE_BGR: - madctl |= MADCTL_BGR - sequence.append((MADCTL, madctl)) - if INVON not in commands and INVOFF not in commands: - if config[CONF_INVERT_COLORS]: - sequence.append((INVON,)) - else: - sequence.append((INVOFF,)) - if BRIGHTNESS not in commands: - if brightness := config.get( - CONF_BRIGHTNESS, model.get_default(CONF_BRIGHTNESS) - ): - sequence.append((BRIGHTNESS, brightness)) - if SLPOUT not in commands: - sequence.append((SLPOUT,)) - sequence.append((DISPON,)) + model = MODELS[config[CONF_MODEL]] + width, height, offset_width, offset_height = model.get_dimensions(config) - # Flatten the sequence into a list of bytes, with the length of each command - # or the delay flag inserted where needed - return sum( - tuple( - (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] - for x in sequence - ), - (), - ) + 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 model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0) + ] + templateargs = [ + buffer_type, + bufferpixels, + config[CONF_BYTE_ORDER] == "big_endian", + display_pixel_mode, + bus_type, + width, + height, + offset_width, + offset_height, + ] + # If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi + if requires_buffer(config): + templateargs.append(rotation) + templateargs.append(frac) + 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 - ) - cg.add(var.set_init_sequence(get_sequence(model, config))) - if rotation_as_transform(model, config): + var_id = config[CONF_ID] + var_id.type, templateargs = get_instance(config) + var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) + init_sequence, _madctl = model.get_sequence(config) + cg.add(var.set_init_sequence(init_sequence)) + if model.rotation_as_transform(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 +435,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/__init__.py b/esphome/components/mipi_spi/models/__init__.py index e9726032d4..e69de29bb2 100644 --- a/esphome/components/mipi_spi/models/__init__.py +++ b/esphome/components/mipi_spi/models/__init__.py @@ -1,65 +0,0 @@ -from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE -import esphome.config_validation as cv -from esphome.const import CONF_HEIGHT, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, CONF_WIDTH - -from .. import CONF_NATIVE_HEIGHT, CONF_NATIVE_WIDTH - -MADCTL_MY = 0x80 # Bit 7 Bottom to top -MADCTL_MX = 0x40 # Bit 6 Right to left -MADCTL_MV = 0x20 # Bit 5 Reverse Mode -MADCTL_ML = 0x10 # Bit 4 LCD refresh Bottom to top -MADCTL_RGB = 0x00 # Bit 3 Red-Green-Blue pixel order -MADCTL_BGR = 0x08 # Bit 3 Blue-Green-Red pixel order -MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left - -# These bits are used instead of the above bits on some chips, where using MX and MY results in incorrect -# partial updates. -MADCTL_XFLIP = 0x02 # Mirror the display horizontally -MADCTL_YFLIP = 0x01 # Mirror the display vertically - -DELAY_FLAG = 0xFFF # Special flag to indicate a delay - - -def delay(ms): - return DELAY_FLAG, ms - - -class DriverChip: - models = {} - - def __init__( - self, - name: str, - modes=(TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL), - initsequence=None, - **defaults, - ): - name = name.upper() - self.name = name - self.modes = modes - self.initsequence = initsequence - self.defaults = defaults - DriverChip.models[name] = self - - def extend(self, name, **kwargs): - defaults = self.defaults.copy() - if ( - CONF_WIDTH in defaults - and CONF_OFFSET_WIDTH in kwargs - and CONF_NATIVE_WIDTH not in defaults - ): - defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH] - if ( - CONF_HEIGHT in defaults - and CONF_OFFSET_HEIGHT in kwargs - and CONF_NATIVE_HEIGHT not in defaults - ): - defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT] - defaults.update(kwargs) - return DriverChip(name, self.modes, initsequence=self.initsequence, **defaults) - - def get_default(self, key, fallback=False): - return self.defaults.get(key, fallback) - - def option(self, name, fallback=False): - return cv.Optional(name, default=self.get_default(name, fallback)) 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..bc95fc7f71 100644 --- a/esphome/components/mipi_spi/models/amoled.py +++ b/esphome/components/mipi_spi/models/amoled.py @@ -1,9 +1,22 @@ +from esphome.components.mipi import ( + MIPI, + MODE_RGB, + NORON, + PAGESEL, + PIXFMT, + SLPOUT, + SPIMODESEL, + SWIRE1, + SWIRE2, + TEON, + WCE, + WRAM, + WRCTRLD, + DriverChip, + delay, +) from esphome.components.spi import TYPE_QUAD -from .. import MODE_RGB -from . import DriverChip, delay -from .commands import MIPI, NORON, PAGESEL, PIXFMT, SLPOUT, SWIRE1, SWIRE2, TEON, WRAM - DriverChip( "T-DISPLAY-S3-AMOLED", width=240, @@ -67,6 +80,29 @@ 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, +) + +CO5300 = DriverChip( + "CO5300", + brightness=0xD0, + color_order=MODE_RGB, + bus_mode=TYPE_QUAD, + initsequence=( + (SLPOUT,), # Requires early SLPOUT + (PAGESEL, 0x00), + (SPIMODESEL, 0x80), + (WRCTRLD, 0x20), + (WCE, 0x00), + ), +) + models = {} diff --git a/esphome/components/mipi_spi/models/commands.py b/esphome/components/mipi_spi/models/commands.py deleted file mode 100644 index 032a6e6b2b..0000000000 --- a/esphome/components/mipi_spi/models/commands.py +++ /dev/null @@ -1,82 +0,0 @@ -# MIPI DBI commands - -NOP = 0x00 -SWRESET = 0x01 -RDDID = 0x04 -RDDST = 0x09 -RDMODE = 0x0A -RDMADCTL = 0x0B -RDPIXFMT = 0x0C -RDIMGFMT = 0x0D -RDSELFDIAG = 0x0F -SLEEP_IN = 0x10 -SLPIN = 0x10 -SLEEP_OUT = 0x11 -SLPOUT = 0x11 -PTLON = 0x12 -NORON = 0x13 -INVERT_OFF = 0x20 -INVOFF = 0x20 -INVERT_ON = 0x21 -INVON = 0x21 -ALL_ON = 0x23 -WRAM = 0x24 -GAMMASET = 0x26 -MIPI = 0x26 -DISPOFF = 0x28 -DISPON = 0x29 -CASET = 0x2A -PASET = 0x2B -RASET = 0x2B -RAMWR = 0x2C -WDATA = 0x2C -RAMRD = 0x2E -PTLAR = 0x30 -VSCRDEF = 0x33 -TEON = 0x35 -MADCTL = 0x36 -MADCTL_CMD = 0x36 -VSCRSADD = 0x37 -IDMOFF = 0x38 -IDMON = 0x39 -COLMOD = 0x3A -PIXFMT = 0x3A -GETSCANLINE = 0x45 -BRIGHTNESS = 0x51 -WRDISBV = 0x51 -RDDISBV = 0x52 -WRCTRLD = 0x53 -SWIRE1 = 0x5A -SWIRE2 = 0x5B -IFMODE = 0xB0 -FRMCTR1 = 0xB1 -FRMCTR2 = 0xB2 -FRMCTR3 = 0xB3 -INVCTR = 0xB4 -DFUNCTR = 0xB6 -ETMOD = 0xB7 -PWCTR1 = 0xC0 -PWCTR2 = 0xC1 -PWCTR3 = 0xC2 -PWCTR4 = 0xC3 -PWCTR5 = 0xC4 -VMCTR1 = 0xC5 -IFCTR = 0xC6 -VMCTR2 = 0xC7 -GMCTR = 0xC8 -SETEXTC = 0xC8 -PWSET = 0xD0 -VMCTR = 0xD1 -PWSETN = 0xD2 -RDID4 = 0xD3 -RDINDEX = 0xD9 -RDID1 = 0xDA -RDID2 = 0xDB -RDID3 = 0xDC -RDIDX = 0xDD -GMCTRP1 = 0xE0 -GMCTRN1 = 0xE1 -CSCON = 0xF0 -PWCTR6 = 0xF6 -ADJCTL3 = 0xF7 -PAGESEL = 0xFE diff --git a/esphome/components/mipi_spi/models/ili.py b/esphome/components/mipi_spi/models/ili.py index cc12b38f5d..0102c0f665 100644 --- a/esphome/components/mipi_spi/models/ili.py +++ b/esphome/components/mipi_spi/models/ili.py @@ -1,8 +1,4 @@ -from esphome.components.spi import TYPE_OCTAL - -from .. import MODE_RGB -from . import DriverChip, delay -from .commands import ( +from esphome.components.mipi import ( ADJCTL3, CSCON, DFUNCTR, @@ -18,6 +14,7 @@ from .commands import ( IFCTR, IFMODE, INVCTR, + MODE_RGB, NORON, PWCTR1, PWCTR2, @@ -32,7 +29,10 @@ from .commands import ( VMCTR1, VMCTR2, VSCRSADD, + DriverChip, + delay, ) +from esphome.components.spi import TYPE_OCTAL DriverChip( "M5CORE", diff --git a/esphome/components/mipi_spi/models/jc.py b/esphome/components/mipi_spi/models/jc.py index 449c5b87ae..f1f046a427 100644 --- a/esphome/components/mipi_spi/models/jc.py +++ b/esphome/components/mipi_spi/models/jc.py @@ -1,10 +1,8 @@ +from esphome.components.mipi import MODE_RGB, DriverChip from esphome.components.spi import TYPE_QUAD import esphome.config_validation as cv from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER -from .. import MODE_RGB -from . import DriverChip - AXS15231 = DriverChip( "AXS15231", draw_rounding=8, diff --git a/esphome/components/mipi_spi/models/lilygo.py b/esphome/components/mipi_spi/models/lilygo.py index dd6f9c02f7..13ddc67465 100644 --- a/esphome/components/mipi_spi/models/lilygo.py +++ b/esphome/components/mipi_spi/models/lilygo.py @@ -1,6 +1,6 @@ +from esphome.components.mipi import MODE_BGR from esphome.components.spi import TYPE_OCTAL -from .. import MODE_BGR from .ili import ST7789V, ST7796 ST7789V.extend( diff --git a/esphome/components/mipi_spi/models/waveshare.py b/esphome/components/mipi_spi/models/waveshare.py index 6d14f56fc6..7a55027e58 100644 --- a/esphome/components/mipi_spi/models/waveshare.py +++ b/esphome/components/mipi_spi/models/waveshare.py @@ -1,4 +1,7 @@ -from . import DriverChip +from esphome.components.mipi import DriverChip +import esphome.config_validation as cv + +from .amoled import CO5300 from .ili import ILI9488_A DriverChip( @@ -128,6 +131,7 @@ DriverChip( ILI9488_A.extend( "PICO-RESTOUCH-LCD-3.5", + swap_xy=cv.UNDEFINED, spi_16=True, pixel_mode="16bit", mirror_x=True, @@ -137,3 +141,14 @@ ILI9488_A.extend( data_rate="20MHz", invert_colors=True, ) + +CO5300.extend( + "WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.75", + width=466, + height=466, + pixel_mode="16bit", + offset_height=0, + offset_width=6, + cs_pin=12, + reset_pin=39, +) diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index a451f2b7b4..46729f8eda 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -124,11 +124,10 @@ async def to_code(config): if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM): cg.add(var.set_task_stack_in_psram(task_stack_in_psram)) - if task_stack_in_psram: - if config[CONF_TASK_STACK_IN_PSRAM]: - esp32.add_idf_sdkconfig_option( - "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True - ) + if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]: + esp32.add_idf_sdkconfig_option( + "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True + ) for speaker_config in config[CONF_SOURCE_SPEAKERS]: source_speaker = cg.new_Pvariable(speaker_config[CONF_ID]) diff --git a/esphome/components/mlx90393/sensor.py b/esphome/components/mlx90393/sensor.py index 372bb05bda..d93c379506 100644 --- a/esphome/components/mlx90393/sensor.py +++ b/esphome/components/mlx90393/sensor.py @@ -63,11 +63,13 @@ def _validate(config): raise cv.Invalid( f"{axis}: {CONF_RESOLUTION} cannot be {res} with {CONF_TEMPERATURE_COMPENSATION} enabled" ) - if config[CONF_HALLCONF] == 0xC: - if (config[CONF_OVERSAMPLING], config[CONF_FILTER]) in [(0, 0), (1, 0), (0, 1)]: - raise cv.Invalid( - f"{CONF_OVERSAMPLING}=={config[CONF_OVERSAMPLING]} and {CONF_FILTER}=={config[CONF_FILTER]} not allowed with {CONF_HALLCONF}=={config[CONF_HALLCONF]:#02x}" - ) + if config[CONF_HALLCONF] == 0xC and ( + config[CONF_OVERSAMPLING], + config[CONF_FILTER], + ) in [(0, 0), (1, 0), (0, 1)]: + raise cv.Invalid( + f"{CONF_OVERSAMPLING}=={config[CONF_OVERSAMPLING]} and {CONF_FILTER}=={config[CONF_FILTER]} not allowed with {CONF_HALLCONF}=={config[CONF_HALLCONF]:#02x}" + ) return config diff --git a/esphome/components/mlx90393/sensor_mlx90393.cpp b/esphome/components/mlx90393/sensor_mlx90393.cpp index 96749cd378..21a5b3a829 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.cpp +++ b/esphome/components/mlx90393/sensor_mlx90393.cpp @@ -103,7 +103,6 @@ bool MLX90393Cls::apply_all_settings_() { } void MLX90393Cls::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // note the two arguments A0 and A1 which are used to construct an i2c address // we can hard-code these because we never actually use the constructed address // see the transceive function above, which uses the address from I2CComponent diff --git a/esphome/components/mlx90614/mlx90614.cpp b/esphome/components/mlx90614/mlx90614.cpp index afc565d38b..2e711baf9a 100644 --- a/esphome/components/mlx90614/mlx90614.cpp +++ b/esphome/components/mlx90614/mlx90614.cpp @@ -28,7 +28,6 @@ static const uint8_t MLX90614_ID4 = 0x3F; static const char *const TAG = "mlx90614"; void MLX90614Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (!this->write_emissivity_()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); this->mark_failed(); diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index 7f78f9592a..d712e2401d 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -31,7 +31,6 @@ static const uint8_t MMC56X3_CTRL2_REG = 0x1D; static const uint8_t MMC5603_ODR_REG = 0x1A; void MMC5603Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t id = 0; if (!this->read_byte(MMC56X3_PRODUCT_ID, &id)) { this->error_code_ = COMMUNICATION_FAILED; diff --git a/esphome/components/mmc5983/mmc5983.cpp b/esphome/components/mmc5983/mmc5983.cpp index d5394da618..1e0065020c 100644 --- a/esphome/components/mmc5983/mmc5983.cpp +++ b/esphome/components/mmc5983/mmc5983.cpp @@ -67,8 +67,6 @@ void MMC5983Component::update() { } void MMC5983Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Verify product id. const uint8_t mmc5983_product_id = 0x30; uint8_t id; diff --git a/esphome/components/mpl3115a2/mpl3115a2.cpp b/esphome/components/mpl3115a2/mpl3115a2.cpp index 9b65fb04e4..9e8467a29b 100644 --- a/esphome/components/mpl3115a2/mpl3115a2.cpp +++ b/esphome/components/mpl3115a2/mpl3115a2.cpp @@ -9,8 +9,6 @@ namespace mpl3115a2 { static const char *const TAG = "mpl3115a2"; void MPL3115A2Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - uint8_t whoami = 0xFF; if (!this->read_byte(MPL3115A2_WHOAMI, &whoami, false)) { this->error_code_ = COMMUNICATION_FAILED; diff --git a/esphome/components/mpr121/__init__.py b/esphome/components/mpr121/__init__.py index b736a7e4f0..0bf9377275 100644 --- a/esphome/components/mpr121/__init__.py +++ b/esphome/components/mpr121/__init__.py @@ -56,12 +56,13 @@ def _final_validate(config): for binary_sensor in binary_sensors: if binary_sensor.get(CONF_MPR121_ID) == config[CONF_ID]: max_touch_channel = max(max_touch_channel, binary_sensor[CONF_CHANNEL]) - if max_touch_channel_in_config := config.get(CONF_MAX_TOUCH_CHANNEL): - if max_touch_channel != max_touch_channel_in_config: - raise cv.Invalid( - "Max touch channel must equal the highest binary sensor channel or be removed for auto calculation", - path=[CONF_MAX_TOUCH_CHANNEL], - ) + if ( + max_touch_channel_in_config := config.get(CONF_MAX_TOUCH_CHANNEL) + ) and max_touch_channel != max_touch_channel_in_config: + raise cv.Invalid( + "Max touch channel must equal the highest binary sensor channel or be removed for auto calculation", + path=[CONF_MAX_TOUCH_CHANNEL], + ) path = fconf.get_path_for_id(config[CONF_ID])[:-1] this_config = fconf.get_config_for_path(path) this_config[CONF_MAX_TOUCH_CHANNEL] = max_touch_channel diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index 39c45d7a89..074bc79ea2 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -11,7 +11,6 @@ namespace mpr121 { static const char *const TAG = "mpr121"; void MPR121Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // soft reset device this->write_byte(MPR121_SOFTRESET, 0x63); delay(100); // NOLINT diff --git a/esphome/components/mpu6050/mpu6050.cpp b/esphome/components/mpu6050/mpu6050.cpp index 84f0fb4bae..ecbee11c48 100644 --- a/esphome/components/mpu6050/mpu6050.cpp +++ b/esphome/components/mpu6050/mpu6050.cpp @@ -21,7 +21,6 @@ const uint8_t MPU6050_BIT_TEMPERATURE_DISABLED = 3; const float GRAVITY_EARTH = 9.80665f; void MPU6050Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t who_am_i; if (!this->read_byte(MPU6050_REGISTER_WHO_AM_I, &who_am_i) || (who_am_i != 0x68 && who_am_i != 0x70 && who_am_i != 0x98)) { diff --git a/esphome/components/mpu6886/mpu6886.cpp b/esphome/components/mpu6886/mpu6886.cpp index cbd8b601bd..6fdf7b8684 100644 --- a/esphome/components/mpu6886/mpu6886.cpp +++ b/esphome/components/mpu6886/mpu6886.cpp @@ -26,7 +26,6 @@ const float TEMPERATURE_SENSITIVITY = 326.8; const float TEMPERATURE_OFFSET = 25.0; void MPU6886Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t who_am_i; if (!this->read_byte(MPU6886_REGISTER_WHO_AM_I, &who_am_i) || who_am_i != MPU6886_WHO_AM_I_IDENTIFIER) { this->mark_failed(); diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 1a6fcabf42..52d3181780 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -312,14 +312,13 @@ CONFIG_SCHEMA = cv.All( def exp_mqtt_message(config): if config is None: return cg.optional(cg.TemplateArguments(MQTTMessage)) - exp = cg.StructInitializer( + return cg.StructInitializer( MQTTMessage, ("topic", config[CONF_TOPIC]), ("payload", config.get(CONF_PAYLOAD, "")), ("qos", config[CONF_QOS]), ("retain", config[CONF_RETAIN]), ) - return exp @coroutine_with_priority(40.0) diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index c619a02344..b3435edf38 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -27,7 +27,7 @@ void MQTTButtonComponent::setup() { } void MQTTButtonComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Button '%s': ", this->button_->get_name().c_str()); - LOG_MQTT_COMPONENT(true, true); + LOG_MQTT_COMPONENT(false, true); } void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index f3e57a66be..7675280f1a 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -34,7 +34,6 @@ MQTTClientComponent::MQTTClientComponent() { // Connection void MQTTClientComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->mqtt_backend_.set_on_message( [this](const char *topic, const char *payload, size_t len, size_t index, size_t total) { if (index == 0) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index b51f4d903e..6ceaf219ff 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -129,21 +129,16 @@ bool MQTTComponent::send_discovery_() { root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available; } - std::string unique_id = this->unique_id(); const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); - if (!unique_id.empty()) { - root[MQTT_UNIQUE_ID] = unique_id; + if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { + char friendly_name_hash[9]; + sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name())); + friendly_name_hash[8] = 0; // ensure the hash-string ends with null + root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; } else { - if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { - char friendly_name_hash[9]; - sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name())); - friendly_name_hash[8] = 0; // ensure the hash-string ends with null - root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; - } else { - // default to almost-unique ID. It's a hack but the only way to get that - // gorgeous device registry view. - root[MQTT_UNIQUE_ID] = "ESP" + this->component_type() + this->get_default_object_id_(); - } + // default to almost-unique ID. It's a hack but the only way to get that + // gorgeous device registry view. + root[MQTT_UNIQUE_ID] = "ESP" + this->component_type() + this->get_default_object_id_(); } const std::string &node_name = App.get_name(); @@ -286,7 +281,6 @@ void MQTTComponent::call_dump_config() { this->dump_config(); } void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } -std::string MQTTComponent::unique_id() { return ""; } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } // Pull these properties from EntityBase if not overridden diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 01ba98ad40..851fdd842c 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -164,13 +164,6 @@ class MQTTComponent : public Component { */ virtual const EntityBase *get_entity() const = 0; - /** A unique ID for this MQTT component, empty for no unique id. See unique ID requirements: - * https://developers.home-assistant.io/docs/en/entity_registry_index.html#unique-id-requirements - * - * @return The unique id as a string. - */ - virtual std::string unique_id(); - /// Get the friendly name of this MQTT component. virtual std::string friendly_name() const; diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 9324ea9bb1..2e1db1908f 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -76,7 +76,6 @@ bool MQTTSensorComponent::publish_state(float value) { int8_t accuracy = this->sensor_->get_accuracy_decimals(); return this->publish(this->get_state_topic_(), value_accuracy_to_string(value, accuracy)); } -std::string MQTTSensorComponent::unique_id() { return this->sensor_->unique_id(); } } // namespace mqtt } // namespace esphome diff --git a/esphome/components/mqtt/mqtt_sensor.h b/esphome/components/mqtt/mqtt_sensor.h index adc201736a..15ea703ad4 100644 --- a/esphome/components/mqtt/mqtt_sensor.h +++ b/esphome/components/mqtt/mqtt_sensor.h @@ -46,7 +46,6 @@ class MQTTSensorComponent : public mqtt::MQTTComponent { /// Override for MQTTComponent, returns "sensor". std::string component_type() const override; const EntityBase *get_entity() const override; - std::string unique_id() override; sensor::Sensor *sensor_; optional expire_after_; // Override the expire after advertised to Home Assistant diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index 0cc5de07a3..42260ed2a8 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -40,7 +40,6 @@ bool MQTTTextSensor::send_initial_state() { } std::string MQTTTextSensor::component_type() const { return "sensor"; } const EntityBase *MQTTTextSensor::get_entity() const { return this->sensor_; } -std::string MQTTTextSensor::unique_id() { return this->sensor_->unique_id(); } } // namespace mqtt } // namespace esphome diff --git a/esphome/components/mqtt/mqtt_text_sensor.h b/esphome/components/mqtt/mqtt_text_sensor.h index fe53a6fefd..9a14efdd16 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.h +++ b/esphome/components/mqtt/mqtt_text_sensor.h @@ -28,7 +28,6 @@ class MQTTTextSensor : public mqtt::MQTTComponent { protected: std::string component_type() const override; const EntityBase *get_entity() const override; - std::string unique_id() override; text_sensor::TextSensor *sensor_; }; diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 7a820f3b5a..8f8c05eb7d 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -15,7 +15,6 @@ static const uint8_t MS5611_CMD_CONV_D2 = 0x50; static const uint8_t MS5611_CMD_READ_PROM = 0xA2; void MS5611Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (!this->write_bytes(MS5611_CMD_RESET, nullptr, 0)) { this->mark_failed(); return; diff --git a/esphome/components/ms8607/ms8607.cpp b/esphome/components/ms8607/ms8607.cpp index f8ea26bfd9..215131eb8e 100644 --- a/esphome/components/ms8607/ms8607.cpp +++ b/esphome/components/ms8607/ms8607.cpp @@ -67,7 +67,6 @@ static uint8_t crc4(uint16_t *buffer, size_t length); static uint8_t hsensor_crc_check(uint16_t value); void MS8607Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->error_code_ = ErrorCode::NONE; this->setup_status_ = SetupStatus::NEEDS_RESET; diff --git a/esphome/components/msa3xx/msa3xx.cpp b/esphome/components/msa3xx/msa3xx.cpp index 17f0a9c418..56dc919968 100644 --- a/esphome/components/msa3xx/msa3xx.cpp +++ b/esphome/components/msa3xx/msa3xx.cpp @@ -118,8 +118,6 @@ const char *orientation_xy_to_string(OrientationXY orientation) { const char *orientation_z_to_string(bool orientation) { return orientation ? "Downwards looking" : "Upwards looking"; } void MSA3xxComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - uint8_t part_id{0xff}; if (!this->read_byte(static_cast(RegisterMap::PART_ID), &part_id) || (part_id != MSA_3XX_PART_ID)) { ESP_LOGE(TAG, "Part ID is wrong or missing. Got 0x%02X", part_id); diff --git a/esphome/components/my9231/my9231.cpp b/esphome/components/my9231/my9231.cpp index 691c945254..fba7ac2bf3 100644 --- a/esphome/components/my9231/my9231.cpp +++ b/esphome/components/my9231/my9231.cpp @@ -28,7 +28,6 @@ static const uint8_t MY9231_CMD_SCATTER_APDM = 0x0 << 0; static const uint8_t MY9231_CMD_SCATTER_PWM = 0x1 << 0; void MY9231OutputComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->pin_di_->setup(); this->pin_di_->digital_write(false); this->pin_dcki_->setup(); @@ -57,7 +56,6 @@ void MY9231OutputComponent::setup() { this->send_dcki_pulses_(32 * this->num_chips_); this->init_chips_(command); } - ESP_LOGV(TAG, " Chips initialized."); } void MY9231OutputComponent::dump_config() { ESP_LOGCONFIG(TAG, "MY9231:"); diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index edcd114852..acdca03fdb 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -52,7 +52,6 @@ static const uint8_t POWER_PGA_CAP_EN = 0x80; static const uint8_t DEVICE_REV = 0x1F; void NAU7802Sensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); i2c::I2CRegister pu_ctrl = this->reg(PU_CTRL_REG); uint8_t rev; diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index c63790e60b..0c9604e932 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -225,6 +225,9 @@ async def to_code(config): # https://github.com/Makuna/NeoPixelBus/blob/master/library.json # Version Listed Here: https://registry.platformio.org/libraries/makuna/NeoPixelBus/versions if CORE.is_esp32: + # disable built in rgb support as it uses the new RMT drivers and will + # conflict with NeoPixelBus which uses the legacy drivers + cg.add_build_flag("-DESP32_ARDUINO_NO_RGB_BUILTIN") cg.add_library("makuna/NeoPixelBus", "2.8.0") else: cg.add_library("makuna/NeoPixelBus", "2.7.3") diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index a8e792a2d7..bf76aefc30 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -13,15 +13,27 @@ #include "esphome/components/openthread/openthread.h" #endif +#ifdef USE_MODEM +#include "esphome/components/modem/modem_component.h" +#endif + namespace esphome { namespace network { +// The order of the components is important: WiFi should come after any possible main interfaces (it may be used as +// an AP that use a previous interface for NAT). + bool is_connected() { #ifdef USE_ETHERNET if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected()) return true; #endif +#ifdef USE_MODEM + if (modem::global_modem_component != nullptr) + return modem::global_modem_component->is_connected(); +#endif + #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) return wifi::global_wifi_component->is_connected(); @@ -39,6 +51,11 @@ bool is_connected() { } bool is_disabled() { +#ifdef USE_MODEM + if (modem::global_modem_component != nullptr) + return modem::global_modem_component->is_disabled(); +#endif + #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) return wifi::global_wifi_component->is_disabled(); @@ -51,6 +68,12 @@ network::IPAddresses get_ip_addresses() { if (ethernet::global_eth_component != nullptr) return ethernet::global_eth_component->get_ip_addresses(); #endif + +#ifdef USE_MODEM + if (modem::global_modem_component != nullptr) + return modem::global_modem_component->get_ip_addresses(); +#endif + #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) return wifi::global_wifi_component->get_ip_addresses(); @@ -67,6 +90,12 @@ std::string get_use_address() { if (ethernet::global_eth_component != nullptr) return ethernet::global_eth_component->get_use_address(); #endif + +#ifdef USE_MODEM + if (modem::global_modem_component != nullptr) + return modem::global_modem_component->get_use_address(); +#endif + #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) return wifi::global_wifi_component->get_use_address(); diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 66e2d26061..133bd2947c 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -450,7 +450,6 @@ void Nextion::process_nextion_commands_() { this->remove_from_q_(); if (!this->is_setup_) { if (this->nextion_queue_.empty()) { - ESP_LOGD(TAG, "Setup complete"); this->is_setup_ = true; this->setup_callback_.call(); } diff --git a/esphome/components/nfc/binary_sensor/binary_sensor.cpp b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp similarity index 99% rename from esphome/components/nfc/binary_sensor/binary_sensor.cpp rename to esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp index 8f1f6acd51..bc19fa7213 100644 --- a/esphome/components/nfc/binary_sensor/binary_sensor.cpp +++ b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp @@ -1,4 +1,4 @@ -#include "binary_sensor.h" +#include "nfc_binary_sensor.h" #include "../nfc_helpers.h" #include "esphome/core/log.h" diff --git a/esphome/components/nfc/binary_sensor/binary_sensor.h b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.h similarity index 100% rename from esphome/components/nfc/binary_sensor/binary_sensor.h rename to esphome/components/nfc/binary_sensor/nfc_binary_sensor.h diff --git a/esphome/components/npi19/npi19.cpp b/esphome/components/npi19/npi19.cpp index 17ca0ef23e..e8c4e8abd5 100644 --- a/esphome/components/npi19/npi19.cpp +++ b/esphome/components/npi19/npi19.cpp @@ -11,8 +11,6 @@ static const char *const TAG = "npi19"; static const uint8_t READ_COMMAND = 0xAC; void NPI19Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - uint16_t raw_temperature(0); uint16_t raw_pressure(0); i2c::ErrorCode err = this->read_(raw_temperature, raw_pressure); diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py new file mode 100644 index 0000000000..908a855f70 --- /dev/null +++ b/esphome/components/nrf52/__init__.py @@ -0,0 +1,223 @@ +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, + ThreadModel, +) +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") + # nRF52 processors are single-core + cg.add_define(ThreadModel.SINGLE) + 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_MCUBOOT: + cg.add_define("USE_BOOTLOADER_MCUBOOT") + else: + # 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..715d527a66 --- /dev/null +++ b/esphome/components/nrf52/const.py @@ -0,0 +1,18 @@ +BOOTLOADER_ADAFRUIT = "adafruit" +BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132" +BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6" +BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7" +EXTRA_ADC = [ + "VDD", + "VDDHDIV5", +] +AIN_TO_GPIO = { + "AIN0": 2, + "AIN1": 3, + "AIN2": 4, + "AIN3": 5, + "AIN4": 28, + "AIN5": 29, + "AIN6": 30, + "AIN7": 31, +} diff --git a/esphome/components/nrf52/gpio.py b/esphome/components/nrf52/gpio.py new file mode 100644 index 0000000000..260114f90e --- /dev/null +++ b/esphome/components/nrf52/gpio.py @@ -0,0 +1,79 @@ +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_ANALOG, + CONF_ID, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + PLATFORM_NRF52, +) + +from .const import AIN_TO_GPIO, EXTRA_ADC + +ZephyrGPIOPin = zephyr_ns.class_("ZephyrGPIOPin", cg.InternalGPIOPin) + + +def _translate_pin(value): + if value in AIN_TO_GPIO: + return AIN_TO_GPIO[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): + if value in EXTRA_ADC: + return value + value = _translate_pin(value) + if value < 0 or value > (32 + 16): + raise cv.Invalid(f"NRF52: Invalid pin number: {value}") + return value + + +def validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_analog = mode[CONF_ANALOG] + if is_analog: + if num in EXTRA_ADC: + return value + if num not in AIN_TO_GPIO.values(): + raise cv.Invalid(f"Cannot use {num} as analog pin") + return value + + +NRF52_PIN_SCHEMA = cv.All( + pins.gpio_base_schema( + ZephyrGPIOPin, + validate_gpio_pin, + modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,), + ), + validate_supports, +) + + +@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..4a83d5fc5f 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, @@ -321,7 +323,6 @@ async def number_in_range_to_code(config, condition_id, template_arg, args): @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_NUMBER") cg.add_global(number_ns.using) diff --git a/esphome/components/one_wire/__init__.py b/esphome/components/one_wire/__init__.py index 99a1ccd1eb..6d95b8fd33 100644 --- a/esphome/components/one_wire/__init__.py +++ b/esphome/components/one_wire/__init__.py @@ -18,13 +18,12 @@ def one_wire_device_schema(): :return: The 1-wire device schema, `extend` this in your config schema. """ - schema = cv.Schema( + return cv.Schema( { cv.GenerateID(CONF_ONE_WIRE_ID): cv.use_id(OneWireBus), cv.Optional(CONF_ADDRESS): cv.hex_uint64_t, } ) - return schema async def register_one_wire_device(var, config): 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/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp index 0a4ef98507..b23792fc7a 100644 --- a/esphome/components/opentherm/hub.cpp +++ b/esphome/components/opentherm/hub.cpp @@ -145,7 +145,6 @@ void OpenthermHub::process_response(OpenthermData &data) { } void OpenthermHub::setup() { - ESP_LOGD(TAG, "Setting up OpenTherm component"); this->opentherm_ = make_unique(this->in_pin_, this->out_pin_); if (!this->opentherm_->initialize()) { ESP_LOGE(TAG, "Failed to initialize OpenTherm protocol. See previous log messages for details."); diff --git a/esphome/components/opentherm/number/__init__.py b/esphome/components/opentherm/number/__init__.py index a65864647a..6dbc45f49b 100644 --- a/esphome/components/opentherm/number/__init__.py +++ b/esphome/components/opentherm/number/__init__.py @@ -25,9 +25,9 @@ async def new_openthermnumber(config: dict[str, Any]) -> cg.Pvariable: await cg.register_component(var, config) input.generate_setters(var, config) - if (initial_value := config.get(CONF_INITIAL_VALUE, None)) is not None: + if (initial_value := config.get(CONF_INITIAL_VALUE)) is not None: cg.add(var.set_initial_value(initial_value)) - if (restore_value := config.get(CONF_RESTORE_VALUE, None)) is not None: + if (restore_value := config.get(CONF_RESTORE_VALUE)) is not None: cg.add(var.set_restore_value(restore_value)) return var diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 25e3153d1b..2f085ebaae 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -79,9 +79,8 @@ def set_sdkconfig_options(config): "CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower() ) - if force_dataset := config.get(CONF_FORCE_DATASET): - if force_dataset: - cg.add_define("USE_OPENTHREAD_FORCE_DATASET") + if config.get(CONF_FORCE_DATASET): + cg.add_define("USE_OPENTHREAD_FORCE_DATASET") add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DNS64_CLIENT", True) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 24b3c23960..322ff43238 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -1,6 +1,9 @@ #include "esphome/core/defines.h" #ifdef USE_OPENTHREAD #include "openthread.h" +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) +#include "esp_openthread.h" +#endif #include @@ -28,18 +31,6 @@ OpenThreadComponent *global_openthread_component = // NOLINT(cppcoreguidelines- OpenThreadComponent::OpenThreadComponent() { global_openthread_component = this; } -OpenThreadComponent::~OpenThreadComponent() { - auto lock = InstanceLock::try_acquire(100); - if (!lock) { - ESP_LOGW(TAG, "Failed to acquire OpenThread lock in destructor, leaking memory"); - return; - } - otInstance *instance = lock->get_instance(); - otSrpClientClearHostAndServices(instance); - otSrpClientBuffersFreeAllServices(instance); - global_openthread_component = nullptr; -} - bool OpenThreadComponent::is_connected() { auto lock = InstanceLock::try_acquire(100); if (!lock) { @@ -189,7 +180,6 @@ void OpenThreadSrpComponent::setup() { } otSrpClientEnableAutoStartMode(instance, srp_start_callback, nullptr); - ESP_LOGD(TAG, "Finished SRP setup"); } void *OpenThreadSrpComponent::pool_alloc_(size_t size) { @@ -200,6 +190,33 @@ void *OpenThreadSrpComponent::pool_alloc_(size_t size) { void OpenThreadSrpComponent::set_mdns(esphome::mdns::MDNSComponent *mdns) { this->mdns_ = mdns; } +bool OpenThreadComponent::teardown() { + if (!this->teardown_started_) { + this->teardown_started_ = true; + ESP_LOGD(TAG, "Clear Srp"); + auto lock = InstanceLock::try_acquire(100); + if (!lock) { + ESP_LOGW(TAG, "Failed to acquire OpenThread lock during teardown, leaking memory"); + return true; + } + otInstance *instance = lock->get_instance(); + otSrpClientClearHostAndServices(instance); + otSrpClientBuffersFreeAllServices(instance); + global_openthread_component = nullptr; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + ESP_LOGD(TAG, "Exit main loop "); + int error = esp_openthread_mainloop_exit(); + if (error != ESP_OK) { + ESP_LOGW(TAG, "Failed attempt to stop main loop %d", error); + this->teardown_complete_ = true; + } +#else + this->teardown_complete_ = true; +#endif + } + return this->teardown_complete_; +} + } // namespace openthread } // namespace esphome diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 77fd58851a..a0ea1b3f3a 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -21,6 +21,7 @@ class OpenThreadComponent : public Component { OpenThreadComponent(); ~OpenThreadComponent(); void setup() override; + bool teardown() override; float get_setup_priority() const override { return setup_priority::WIFI; } bool is_connected(); @@ -30,6 +31,8 @@ class OpenThreadComponent : public Component { protected: std::optional get_omr_address_(InstanceLock &lock); + bool teardown_started_{false}; + bool teardown_complete_{false}; }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index dc303cef17..b11b7ad34a 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -28,7 +28,6 @@ namespace esphome { namespace openthread { void OpenThreadComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // Used eventfds: // * netif // * ot task queue @@ -144,10 +143,13 @@ void OpenThreadComponent::ot_main() { esp_openthread_launch_mainloop(); // Clean up + esp_openthread_deinit(); esp_openthread_netif_glue_deinit(); esp_netif_destroy(openthread_netif); esp_vfs_eventfd_unregister(); + this->teardown_complete_ = true; + vTaskDelete(NULL); } network::IPAddresses OpenThreadComponent::get_ip_addresses() { 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/output/__init__.py b/esphome/components/output/__init__.py index 78bfa045e1..bde106b085 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -43,6 +43,8 @@ FloatOutputPtr = FloatOutput.operator("ptr") TurnOffAction = output_ns.class_("TurnOffAction", automation.Action) TurnOnAction = output_ns.class_("TurnOnAction", automation.Action) SetLevelAction = output_ns.class_("SetLevelAction", automation.Action) +SetMinPowerAction = output_ns.class_("SetMinPowerAction", automation.Action) +SetMaxPowerAction = output_ns.class_("SetMaxPowerAction", automation.Action) async def setup_output_platform_(obj, config): @@ -104,6 +106,42 @@ async def output_set_level_to_code(config, action_id, template_arg, args): return var +@automation.register_action( + "output.set_min_power", + SetMinPowerAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(FloatOutput), + cv.Required(CONF_MIN_POWER): cv.templatable(cv.percentage), + } + ), +) +async def output_set_min_power_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_MIN_POWER], args, float) + cg.add(var.set_min_power(template_)) + return var + + +@automation.register_action( + "output.set_max_power", + SetMaxPowerAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(FloatOutput), + cv.Required(CONF_MAX_POWER): cv.templatable(cv.percentage), + } + ), +) +async def output_set_max_power_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_MAX_POWER], args, float) + cg.add(var.set_max_power(template_)) + return var + + async def to_code(config): cg.add_define("USE_OUTPUT") cg.add_global(output_ns.using) diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index 51c2849702..de84bb91ca 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -40,5 +40,29 @@ template class SetLevelAction : public Action { FloatOutput *output_; }; +template class SetMinPowerAction : public Action { + public: + SetMinPowerAction(FloatOutput *output) : output_(output) {} + + TEMPLATABLE_VALUE(float, min_power) + + void play(Ts... x) override { this->output_->set_min_power(this->min_power_.value(x...)); } + + protected: + FloatOutput *output_; +}; + +template class SetMaxPowerAction : public Action { + public: + SetMaxPowerAction(FloatOutput *output) : output_(output) {} + + TEMPLATABLE_VALUE(float, max_power) + + void play(Ts... x) override { this->output_->set_max_power(this->max_power_.value(x...)); } + + protected: + FloatOutput *output_; +}; + } // namespace output } // namespace esphome diff --git a/esphome/components/output/switch/output_switch.cpp b/esphome/components/output/switch/output_switch.cpp index c30cfd3f56..54260ba37a 100644 --- a/esphome/components/output/switch/output_switch.cpp +++ b/esphome/components/output/switch/output_switch.cpp @@ -8,8 +8,6 @@ static const char *const TAG = "output.switch"; void OutputSwitch::dump_config() { LOG_SWITCH("", "Output Switch", this); } void OutputSwitch::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); - bool initial_state = this->get_initial_state_with_restore_mode().value_or(false); if (initial_state) { diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 0db7841db2..2e7dc0e197 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -186,8 +186,7 @@ def _process_package(package_config, config): package_config = _process_base_package(package_config) if isinstance(package_config, dict): recursive_package = do_packages_pass(package_config) - config = merge_config(recursive_package, config) - return config + return merge_config(recursive_package, config) def do_packages_pass(config: dict): diff --git a/esphome/components/packet_transport/__init__.py b/esphome/components/packet_transport/__init__.py index 99c1d824ca..bfb2bbc4f8 100644 --- a/esphome/components/packet_transport/__init__.py +++ b/esphome/components/packet_transport/__init__.py @@ -89,9 +89,10 @@ def validate_(config): raise cv.Invalid("No sensors or binary sensors to encrypt") elif config[CONF_ROLLING_CODE_ENABLE]: raise cv.Invalid("Rolling code requires an encryption key") - if config[CONF_PING_PONG_ENABLE]: - if not any(CONF_ENCRYPTION in p for p in config.get(CONF_PROVIDERS) or ()): - raise cv.Invalid("Ping-pong requires at least one encrypted provider") + if config[CONF_PING_PONG_ENABLE] and not any( + CONF_ENCRYPTION in p for p in config.get(CONF_PROVIDERS) or () + ): + raise cv.Invalid("Ping-pong requires at least one encrypted provider") return config diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index 3e76df5015..dc8662d1a2 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -24,7 +24,6 @@ enum PCA6416AGPIORegisters { static const char *const TAG = "pca6416a"; void PCA6416AComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // Test to see if device exists uint8_t value; if (!this->read_register_(PCA6416A_INPUT0, &value)) { diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index 6b3f2d20af..f77d680bec 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -13,7 +13,6 @@ const uint8_t CONFIG_REG = 3; static const char *const TAG = "pca9554"; void PCA9554Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->reg_width_ = (this->pin_count_ + 7) / 8; // Test to see if device exists if (!this->read_inputs_()) { diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp index 2fe22fd1cc..6df708ac84 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -26,8 +26,6 @@ static const uint8_t PCA9685_MODE1_AUTOINC = 0b00100000; static const uint8_t PCA9685_MODE1_SLEEP = 0b00010000; void PCA9685Output::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - ESP_LOGV(TAG, " Resetting devices"); if (!this->write_bytes(PCA9685_REGISTER_SOFTWARE_RESET, nullptr, 0)) { this->mark_failed(); diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index d58d35019b..cb987c6129 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -10,7 +10,6 @@ namespace pcf85063 { static const char *const TAG = "pcf85063"; void PCF85063Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (!this->read_rtc_()) { this->mark_failed(); } diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index 7dd7a6fea8..27020378a6 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -10,7 +10,6 @@ namespace pcf8563 { static const char *const TAG = "PCF8563"; void PCF8563Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (!this->read_rtc_()) { this->mark_failed(); } diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index dbab0319d7..848fbed484 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -7,7 +7,6 @@ namespace pcf8574 { static const char *const TAG = "pcf8574"; void PCF8574Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (!this->read_gpio_()) { ESP_LOGE(TAG, "PCF8574 not available under 0x%02X", this->address_); this->mark_failed(); diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 55b8edffc8..18acfda934 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -18,7 +18,6 @@ static const uint8_t PI4IOE5V6408_REGISTER_INTERRUPT_STATUS = 0x13; static const char *const TAG = "pi4ioe5v6408"; void PI4IOE5V6408Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (this->reset_) { this->reg(PI4IOE5V6408_REGISTER_DEVICE_ID) |= 0b00000001; this->reg(PI4IOE5V6408_REGISTER_OUT_HIGH_IMPEDENCE) = 0b00000000; diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index 0d00ba0083..929865b480 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -273,7 +273,7 @@ CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( async def to_code(config): paren = await cg.get_variable(config[CONF_PIPSOLAR_ID]) - for type, _ in TYPES.items(): + for type in TYPES: if type in config: conf = config[type] sens = await sensor.new_sensor(conf) diff --git a/esphome/components/pm2005/pm2005.cpp b/esphome/components/pm2005/pm2005.cpp index 57c616c4c6..d8e253a771 100644 --- a/esphome/components/pm2005/pm2005.cpp +++ b/esphome/components/pm2005/pm2005.cpp @@ -39,7 +39,6 @@ static const LogString *pm2005_get_measuring_mode_string(int status) { static inline uint16_t get_sensor_value(const uint8_t *data, uint8_t i) { return data[i] * 0x100 + data[i + 1]; } void PM2005Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (this->sensor_type_ == PM2005) { this->situation_value_index_ = 3; this->pm_1_0_value_index_ = 4; diff --git a/esphome/components/pmsa003i/pmsa003i.cpp b/esphome/components/pmsa003i/pmsa003i.cpp index 4702c0cf5f..4a618586f8 100644 --- a/esphome/components/pmsa003i/pmsa003i.cpp +++ b/esphome/components/pmsa003i/pmsa003i.cpp @@ -19,8 +19,6 @@ static const uint8_t START_CHARACTER_2 = 0x4D; static const uint8_t READ_DATA_RETRY_COUNT = 3; void PMSA003IComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - PM25AQIData data; bool successful_read = this->read_data_(&data); diff --git a/esphome/components/pmwcs3/sensor.py b/esphome/components/pmwcs3/sensor.py index d42338ab6f..075b9b00b5 100644 --- a/esphome/components/pmwcs3/sensor.py +++ b/esphome/components/pmwcs3/sensor.py @@ -114,8 +114,7 @@ PMWCS3_CALIBRATION_SCHEMA = cv.Schema( ) async def pmwcs3_calibration_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, parent) - return var + return cg.new_Pvariable(action_id, template_arg, parent) PMWCS3_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index da5598bf10..ef4022db4b 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -15,8 +15,6 @@ namespace pn532 { static const char *const TAG = "pn532"; void PN532::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Get version data if (!this->write_command_({PN532_COMMAND_VERSION_DATA})) { ESP_LOGW(TAG, "Error sending version command, trying again"); diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp index 2e66d4ed83..0871f7acab 100644 --- a/esphome/components/pn532_spi/pn532_spi.cpp +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -12,12 +12,10 @@ namespace pn532_spi { static const char *const TAG = "pn532_spi"; void PN532Spi::setup() { - ESP_LOGI(TAG, "PN532Spi setup started!"); this->spi_setup(); this->cs_->digital_write(false); delay(10); - ESP_LOGI(TAG, "SPI setup finished!"); PN532::setup(); } diff --git a/esphome/components/power_supply/power_supply.cpp b/esphome/components/power_supply/power_supply.cpp index 6fbadc73ae..5db2122412 100644 --- a/esphome/components/power_supply/power_supply.cpp +++ b/esphome/components/power_supply/power_supply.cpp @@ -7,22 +7,19 @@ namespace power_supply { static const char *const TAG = "power_supply"; void PowerSupply::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->pin_->setup(); this->pin_->digital_write(false); if (this->enable_on_boot_) this->request_high_power(); } void PowerSupply::dump_config() { - ESP_LOGCONFIG(TAG, "Power Supply:"); - LOG_PIN(" Pin: ", this->pin_); ESP_LOGCONFIG(TAG, + "Power Supply:\n" " Time to enable: %" PRIu32 " ms\n" - " Keep on time: %.1f s", - this->enable_time_, this->keep_on_time_ / 1000.0f); - if (this->enable_on_boot_) - ESP_LOGCONFIG(TAG, " Enabled at startup: True"); + " Keep on time: %" PRIu32 " s\n" + " Enable at startup: %s", + this->enable_time_, this->keep_on_time_ / 1000u, YESNO(this->enable_on_boot_)); + LOG_PIN(" Pin: ", this->pin_); } float PowerSupply::get_setup_priority() const { return setup_priority::IO; } @@ -32,7 +29,7 @@ bool PowerSupply::is_enabled() const { return this->active_requests_ != 0; } void PowerSupply::request_high_power() { if (this->active_requests_ == 0) { this->cancel_timeout("power-supply-off"); - ESP_LOGD(TAG, "Enabling power supply."); + ESP_LOGV(TAG, "Enabling"); this->pin_->digital_write(true); delay(this->enable_time_); } @@ -47,7 +44,7 @@ void PowerSupply::unrequest_high_power() { this->active_requests_--; if (this->active_requests_ == 0) { this->set_timeout("power-supply-off", this->keep_on_time_, [this]() { - ESP_LOGD(TAG, "Disabling power supply."); + ESP_LOGV(TAG, "Disabling"); this->pin_->digital_write(false); }); } diff --git a/esphome/components/power_supply/power_supply.h b/esphome/components/power_supply/power_supply.h index 3959f6f299..0387074eb8 100644 --- a/esphome/components/power_supply/power_supply.h +++ b/esphome/components/power_supply/power_supply.h @@ -36,10 +36,10 @@ class PowerSupply : public Component { protected: GPIOPin *pin_; - bool enable_on_boot_{false}; uint32_t enable_time_; uint32_t keep_on_time_; int16_t active_requests_{0}; // use signed integer to make catching negative requests easier. + bool enable_on_boot_{false}; }; class PowerSupplyRequester { diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index 9299cdcd0e..fd7e70a055 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -28,12 +28,13 @@ from esphome.core import CORE import esphome.final_validate as fv CODEOWNERS = ["@esphome/core"] +DOMAIN = "psram" DEPENDENCIES = [PLATFORM_ESP32] _LOGGER = logging.getLogger(__name__) -psram_ns = cg.esphome_ns.namespace("psram") +psram_ns = cg.esphome_ns.namespace(DOMAIN) PsramComponent = psram_ns.class_("PsramComponent", cg.Component) TYPE_QUAD = "quad" diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index bfca0c6a4e..6300d6fe96 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -156,7 +156,6 @@ pulse_counter_t HwPulseCounterStorage::read_raw_value() { #endif // HAS_PCNT void PulseCounterSensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); if (!this->storage_.pulse_counter_setup(this->pin_)) { this->mark_failed(); return; diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index b330758000..dbf67fd2ad 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -49,12 +49,15 @@ def validate_internal_filter(value): [CONF_USE_PCNT], ) - if CORE.is_esp32 and use_pcnt: - if value.get(CONF_INTERNAL_FILTER).total_microseconds > 13: - raise cv.Invalid( - "Maximum internal filter value when using ESP32 hardware PCNT is 13us", - [CONF_INTERNAL_FILTER], - ) + if ( + CORE.is_esp32 + and use_pcnt + and value.get(CONF_INTERNAL_FILTER).total_microseconds > 13 + ): + raise cv.Invalid( + "Maximum internal filter value when using ESP32 hardware PCNT is 13us", + [CONF_INTERNAL_FILTER], + ) return value diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index ef3de069ca..74b7caefb2 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -26,7 +26,6 @@ void PylontechComponent::dump_config() { } void PylontechComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); while (this->available() != 0) { this->read(); } diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index e41d7de644..c9196f2469 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -24,7 +24,6 @@ static const uint8_t QMC5883L_REGISTER_CONTROL_2 = 0x0A; static const uint8_t QMC5883L_REGISTER_PERIOD = 0x0B; void QMC5883LComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // Soft Reset if (!this->write_byte(QMC5883L_REGISTER_CONTROL_2, 1 << 7)) { this->error_code_ = COMMUNICATION_FAILED; diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 4c81e124ba..6c22150f4f 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -348,8 +348,6 @@ void QMP6988Component::calculate_pressure_() { } void QMP6988Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - bool ret; ret = this->device_check_(); if (!ret) { diff --git a/esphome/components/qspi_dbi/display.py b/esphome/components/qspi_dbi/display.py index 5b01bcc6ca..74d837a794 100644 --- a/esphome/components/qspi_dbi/display.py +++ b/esphome/components/qspi_dbi/display.py @@ -73,9 +73,8 @@ def map_sequence(value): def _validate(config): chip = DriverChip.chips[config[CONF_MODEL]] - if not chip.initsequence: - if CONF_INIT_SEQUENCE not in config: - raise cv.Invalid(f"{chip.name} model requires init_sequence") + if not chip.initsequence and CONF_INIT_SEQUENCE not in config: + raise cv.Invalid(f"{chip.name} model requires init_sequence") return config diff --git a/esphome/components/qspi_dbi/qspi_dbi.cpp b/esphome/components/qspi_dbi/qspi_dbi.cpp index 2901d40268..6c95bb7cf2 100644 --- a/esphome/components/qspi_dbi/qspi_dbi.cpp +++ b/esphome/components/qspi_dbi/qspi_dbi.cpp @@ -6,7 +6,6 @@ namespace esphome { namespace qspi_dbi { void QspiDbi::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); if (this->enable_pin_ != nullptr) { this->enable_pin_->setup(); @@ -113,7 +112,6 @@ void QspiDbi::write_init_sequence_() { } this->reset_params_(true); this->setup_complete_ = true; - ESP_LOGCONFIG(TAG, "QSPI_DBI setup complete"); } void QspiDbi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { diff --git a/esphome/components/qwiic_pir/binary_sensor.py b/esphome/components/qwiic_pir/binary_sensor.py index 0a549ccb32..cd3eda5ac8 100644 --- a/esphome/components/qwiic_pir/binary_sensor.py +++ b/esphome/components/qwiic_pir/binary_sensor.py @@ -24,9 +24,8 @@ QwiicPIRComponent = qwiic_pir_ns.class_( def validate_no_debounce_unless_native(config): - if CONF_DEBOUNCE in config: - if config[CONF_DEBOUNCE_MODE] != "NATIVE": - raise cv.Invalid("debounce can only be set if debounce_mode is NATIVE") + if CONF_DEBOUNCE in config and config[CONF_DEBOUNCE_MODE] != "NATIVE": + raise cv.Invalid("debounce can only be set if debounce_mode is NATIVE") return config diff --git a/esphome/components/qwiic_pir/qwiic_pir.cpp b/esphome/components/qwiic_pir/qwiic_pir.cpp index 6a5196f831..c04c0fcc18 100644 --- a/esphome/components/qwiic_pir/qwiic_pir.cpp +++ b/esphome/components/qwiic_pir/qwiic_pir.cpp @@ -7,8 +7,6 @@ namespace qwiic_pir { static const char *const TAG = "qwiic_pir"; void QwiicPIRComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Verify I2C communcation by reading and verifying the chip ID uint8_t chip_id; if (!this->read_byte(QWIIC_PIR_CHIP_ID, &chip_id)) { diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 018b714190..fa8564f614 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -1,4 +1,5 @@ #include "rc522.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" // Based on: @@ -13,30 +14,6 @@ static const char *const TAG = "rc522"; static const uint8_t RESET_COUNT = 5; -std::string format_buffer(uint8_t *b, uint8_t len) { - char buf[32]; - int offset = 0; - for (uint8_t i = 0; i < len; i++) { - const char *format = "%02X"; - if (i + 1 < len) - format = "%02X-"; - offset += sprintf(buf + offset, format, b[i]); - } - return std::string(buf); -} - -std::string format_uid(std::vector &uid) { - char buf[32]; - 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); -} - void RC522::setup() { state_ = STATE_SETUP; // Pull device out of power down / reset state. @@ -215,7 +192,7 @@ void RC522::loop() { ESP_LOGV(TAG, "STATE_READ_SERIAL_DONE -> TIMEOUT (no tag present) %d", status); } else { ESP_LOGW(TAG, "Unexpected response. Read status is %d. Read bytes: %d (%s)", status, back_length_, - format_buffer(buffer_, 9).c_str()); + format_hex_pretty(buffer_, back_length_, '-', false).c_str()); } state_ = STATE_DONE; @@ -239,7 +216,7 @@ void RC522::loop() { std::vector rfid_uid(std::begin(uid_buffer_), std::begin(uid_buffer_) + uid_idx_); uid_idx_ = 0; - // ESP_LOGD(TAG, "Processing '%s'", format_uid(rfid_uid).c_str()); + // ESP_LOGD(TAG, "Processing '%s'", format_hex_pretty(rfid_uid, '-', false).c_str()); pcd_antenna_off_(); state_ = STATE_INIT; // scan again on next update bool report = true; @@ -260,13 +237,13 @@ void RC522::loop() { trigger->process(rfid_uid); if (report) { - ESP_LOGD(TAG, "Found new tag '%s'", format_uid(rfid_uid).c_str()); + ESP_LOGD(TAG, "Found new tag '%s'", format_hex_pretty(rfid_uid, '-', false).c_str()); } break; } case STATE_DONE: { if (!this->current_uid_.empty()) { - ESP_LOGV(TAG, "Tag '%s' removed", format_uid(this->current_uid_).c_str()); + ESP_LOGV(TAG, "Tag '%s' removed", format_hex_pretty(this->current_uid_, '-', false).c_str()); for (auto *trigger : this->triggers_ontagremoved_) trigger->process(this->current_uid_); } @@ -361,7 +338,7 @@ void RC522::pcd_clear_register_bit_mask_(PcdRegister reg, ///< The register to * @return STATUS_OK on success, STATUS_??? otherwise. */ void RC522::pcd_transceive_data_(uint8_t send_len) { - ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_buffer(buffer_, send_len).c_str()); + ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_hex_pretty(buffer_, send_len, '-', false).c_str()); delayMicroseconds(1000); // we need 1 ms delay between antenna on and those communication commands send_len_ = send_len; // Prepare values for BitFramingReg @@ -435,7 +412,8 @@ RC522::StatusCode RC522::await_transceive_() { error_reg_value); // TODO: is this always due to collissions? return STATUS_ERROR; } - ESP_LOGV(TAG, "received %d bytes: %s", back_length_, format_buffer(buffer_ + send_len_, back_length_).c_str()); + ESP_LOGV(TAG, "received %d bytes: %s", back_length_, + format_hex_pretty(buffer_ + send_len_, back_length_, '-', false).c_str()); return STATUS_OK; } @@ -499,7 +477,7 @@ bool RC522BinarySensor::process(std::vector &data) { this->found_ = result; return result; } -void RC522Trigger::process(std::vector &data) { this->trigger(format_uid(data)); } +void RC522Trigger::process(std::vector &data) { this->trigger(format_hex_pretty(data, '-', false)); } } // namespace rc522 } // namespace esphome diff --git a/esphome/components/rc522_spi/rc522_spi.cpp b/esphome/components/rc522_spi/rc522_spi.cpp index fe1f6097e2..23e92be65a 100644 --- a/esphome/components/rc522_spi/rc522_spi.cpp +++ b/esphome/components/rc522_spi/rc522_spi.cpp @@ -10,7 +10,6 @@ namespace rc522_spi { static const char *const TAG = "rc522_spi"; void RC522Spi::setup() { - ESP_LOGI(TAG, "SPI Setup"); this->spi_setup(); RC522::setup(); diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index fc824ef704..8163661c65 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1062,12 +1062,11 @@ def validate_raw_alternating(value): last_negative = None for i, val in enumerate(value): this_negative = val < 0 - if i != 0: - if this_negative == last_negative: - raise cv.Invalid( - f"Values must alternate between being positive and negative, please see index {i} and {i + 1}", - [i], - ) + if i != 0 and this_negative == last_negative: + raise cv.Invalid( + f"Values must alternate between being positive and negative, please see index {i} and {i + 1}", + [i], + ) last_negative = this_negative return value diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index 3e6172c6d6..7e1bd3c457 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -38,7 +38,6 @@ static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_r } void RemoteReceiverComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); rmt_rx_channel_config_t channel; memset(&channel, 0, sizeof(channel)); channel.clk_src = RMT_CLK_SRC_DEFAULT; diff --git a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp b/esphome/components/remote_receiver/remote_receiver_esp8266.cpp index fe935ba227..b8ac29a543 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp8266.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp8266.cpp @@ -31,7 +31,6 @@ void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverCompone } void RemoteReceiverComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->pin_->setup(); auto &s = this->store_; s.filter_us = this->filter_us_; diff --git a/esphome/components/remote_receiver/remote_receiver_libretiny.cpp b/esphome/components/remote_receiver/remote_receiver_libretiny.cpp index 7a6054737e..8d801b37d2 100644 --- a/esphome/components/remote_receiver/remote_receiver_libretiny.cpp +++ b/esphome/components/remote_receiver/remote_receiver_libretiny.cpp @@ -31,7 +31,6 @@ void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverCompone } void RemoteReceiverComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->pin_->setup(); auto &s = this->store_; s.filter_us = this->filter_us_; diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index 47a46ff56b..e79437013f 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( CONF_PIN, CONF_RMT_SYMBOLS, CONF_USE_DMA, + CONF_VALUE, PlatformFramework, ) from esphome.core import CORE @@ -22,11 +23,17 @@ AUTO_LOAD = ["remote_base"] CONF_EOT_LEVEL = "eot_level" CONF_ON_TRANSMIT = "on_transmit" CONF_ON_COMPLETE = "on_complete" +CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID remote_transmitter_ns = cg.esphome_ns.namespace("remote_transmitter") RemoteTransmitterComponent = remote_transmitter_ns.class_( "RemoteTransmitterComponent", remote_base.RemoteTransmitterBase, cg.Component ) +DigitalWriteAction = remote_transmitter_ns.class_( + "DigitalWriteAction", + automation.Action, + cg.Parented.template(RemoteTransmitterComponent), +) MULTI_CONF = True CONFIG_SCHEMA = cv.Schema( @@ -63,6 +70,25 @@ CONFIG_SCHEMA = cv.Schema( } ).extend(cv.COMPONENT_SCHEMA) +DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterComponent), + cv.Required(CONF_VALUE): cv.templatable(cv.boolean), + }, + key=CONF_VALUE, +) + + +@automation.register_action( + "remote_transmitter.digital_write", DigitalWriteAction, DIGITAL_WRITE_ACTION_SCHEMA +) +async def digital_write_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_TRANSMITTER_ID]) + template_ = await cg.templatable(config[CONF_VALUE], args, bool) + cg.add(var.set_value(template_)) + return var + async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) diff --git a/esphome/components/remote_transmitter/automation.h b/esphome/components/remote_transmitter/automation.h new file mode 100644 index 0000000000..75b017ec61 --- /dev/null +++ b/esphome/components/remote_transmitter/automation.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/remote_transmitter/remote_transmitter.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace remote_transmitter { + +template class DigitalWriteAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, value) + void play(Ts... x) override { this->parent_->digital_write(this->value_.value(x...)); } +}; + +} // namespace remote_transmitter +} // namespace esphome diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index f0dab2aaf8..aa1f54911d 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -30,10 +30,11 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; } + void digital_write(bool value); + #if defined(USE_ESP32) void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } - void digital_write(bool value); #endif Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; }; diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 411e380670..119aa81e7e 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -11,7 +11,6 @@ namespace remote_transmitter { static const char *const TAG = "remote_transmitter"; void RemoteTransmitterComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->inverted_ = this->pin_->is_inverted(); this->configure_rmt_(); } diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp index 73a1a7754f..fdd4198773 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp @@ -73,6 +73,8 @@ void RemoteTransmitterComponent::space_(uint32_t usec) { this->target_time_ += usec; } +void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); } + void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { ESP_LOGD(TAG, "Sending remote code"); uint32_t on_time, off_time; diff --git a/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp b/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp index 42bf5bd95b..9ba850090d 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp @@ -75,6 +75,8 @@ void RemoteTransmitterComponent::space_(uint32_t usec) { this->target_time_ += usec; } +void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); } + void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { ESP_LOGD(TAG, "Sending remote code"); uint32_t on_time, off_time; diff --git a/esphome/components/resampler/speaker/__init__.py b/esphome/components/resampler/speaker/__init__.py index 9e9b32476f..def62547b2 100644 --- a/esphome/components/resampler/speaker/__init__.py +++ b/esphome/components/resampler/speaker/__init__.py @@ -90,11 +90,10 @@ async def to_code(config): if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM): cg.add(var.set_task_stack_in_psram(task_stack_in_psram)) - if task_stack_in_psram: - if config[CONF_TASK_STACK_IN_PSRAM]: - esp32.add_idf_sdkconfig_option( - "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True - ) + if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]: + esp32.add_idf_sdkconfig_option( + "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True + ) cg.add(var.set_target_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE])) diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index 5ccca823de..b4770726b4 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -136,8 +136,7 @@ RFBRIDGE_ID_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(RFBridgeComponent)}) @automation.register_action("rf_bridge.learn", RFBridgeLearnAction, RFBRIDGE_ID_SCHEMA) async def rf_bridge_learnx_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_args, paren) - return var + return cg.new_Pvariable(action_id, template_args, paren) @automation.register_action( @@ -149,8 +148,7 @@ async def rf_bridge_start_advanced_sniffing_to_code( config, action_id, template_args, args ): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_args, paren) - return var + return cg.new_Pvariable(action_id, template_args, paren) @automation.register_action( @@ -162,8 +160,7 @@ async def rf_bridge_stop_advanced_sniffing_to_code( config, action_id, template_args, args ): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_args, paren) - return var + return cg.new_Pvariable(action_id, template_args, paren) @automation.register_action( @@ -175,8 +172,7 @@ async def rf_bridge_start_bucket_sniffing_to_code( config, action_id, template_args, args ): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_args, paren) - return var + return cg.new_Pvariable(action_id, template_args, paren) RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema( diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 79bc123597..20ea8d0293 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -129,8 +129,6 @@ void IRAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensorStore } void RotaryEncoderSensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); - int32_t initial_value = 0; switch (this->restore_mode_) { case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO: diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 11ed97831e..46eabb5325 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, + ThreadModel, ) from esphome.core import CORE, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, mkdir_p, read_file, write_file @@ -171,6 +172,7 @@ async def to_code(config): cg.set_cpp_standard("gnu++20") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "RP2040") + cg.add_define(ThreadModel.SINGLE) cg.add_platformio_option("extra_scripts", ["post:post_build.py"]) @@ -204,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_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index 42f7e9cf52..dc0d3c315a 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -40,8 +40,6 @@ void RP2040PIOLEDStripLightOutput::dma_write_complete_handler_() { } void RP2040PIOLEDStripLightOutput::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - size_t buffer_size = this->get_buffer_size_(); RAMAllocator allocator; diff --git a/esphome/components/rp2040_pio_led_strip/light.py b/esphome/components/rp2040_pio_led_strip/light.py index 9107db9b7f..62f7fffdc9 100644 --- a/esphome/components/rp2040_pio_led_strip/light.py +++ b/esphome/components/rp2040_pio_led_strip/light.py @@ -125,8 +125,7 @@ writezero: def time_to_cycles(time_us): cycles_per_us = 57.5 - cycles = round(float(time_us) * cycles_per_us) - return cycles + return round(float(time_us) * cycles_per_us) CONF_PIO = "pio" diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.cpp b/esphome/components/rp2040_pwm/rp2040_pwm.cpp index 40920f9351..ec164b3c05 100644 --- a/esphome/components/rp2040_pwm/rp2040_pwm.cpp +++ b/esphome/components/rp2040_pwm/rp2040_pwm.cpp @@ -16,11 +16,7 @@ namespace rp2040_pwm { static const char *const TAG = "rp2040_pwm"; -void RP2040PWM::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - - this->setup_pwm_(); -} +void RP2040PWM::setup() { this->setup_pwm_(); } void RP2040PWM::setup_pwm_() { pwm_config config = pwm_get_default_config(); diff --git a/esphome/components/rpi_dpi_rgb/display.py b/esphome/components/rpi_dpi_rgb/display.py index c26143d63e..513ed8eb58 100644 --- a/esphome/components/rpi_dpi_rgb/display.py +++ b/esphome/components/rpi_dpi_rgb/display.py @@ -2,6 +2,18 @@ from esphome import pins import esphome.codegen as cg from esphome.components import display from esphome.components.esp32 import const, only_on_variant +from esphome.components.mipi import ( + CONF_DE_PIN, + CONF_HSYNC_BACK_PORCH, + CONF_HSYNC_FRONT_PORCH, + CONF_HSYNC_PULSE_WIDTH, + CONF_PCLK_FREQUENCY, + CONF_PCLK_INVERTED, + CONF_PCLK_PIN, + CONF_VSYNC_BACK_PORCH, + CONF_VSYNC_FRONT_PORCH, + CONF_VSYNC_PULSE_WIDTH, +) import esphome.config_validation as cv from esphome.const import ( CONF_BLUE, @@ -27,18 +39,6 @@ from esphome.const import ( DEPENDENCIES = ["esp32"] -CONF_DE_PIN = "de_pin" -CONF_PCLK_PIN = "pclk_pin" - -CONF_HSYNC_FRONT_PORCH = "hsync_front_porch" -CONF_HSYNC_PULSE_WIDTH = "hsync_pulse_width" -CONF_HSYNC_BACK_PORCH = "hsync_back_porch" -CONF_VSYNC_FRONT_PORCH = "vsync_front_porch" -CONF_VSYNC_PULSE_WIDTH = "vsync_pulse_width" -CONF_VSYNC_BACK_PORCH = "vsync_back_porch" -CONF_PCLK_FREQUENCY = "pclk_frequency" -CONF_PCLK_INVERTED = "pclk_inverted" - rpi_dpi_rgb_ns = cg.esphome_ns.namespace("rpi_dpi_rgb") RPI_DPI_RGB = rpi_dpi_rgb_ns.class_("RpiDpiRgb", display.Display, cg.Component) ColorOrder = display.display_ns.enum("ColorMode") @@ -140,7 +140,6 @@ async def to_code(config): cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) - index = 0 dpins = [] if CONF_RED in config[CONF_DATA_PINS]: red_pins = config[CONF_DATA_PINS][CONF_RED] @@ -158,10 +157,9 @@ async def to_code(config): dpins = dpins[8:16] + dpins[0:8] else: dpins = config[CONF_DATA_PINS] - for pin in dpins: + for index, pin in enumerate(dpins): data_pin = await cg.gpio_pin_expression(pin) cg.add(var.add_data_pin(data_pin, index)) - index += 1 if enable_pin := config.get(CONF_ENABLE_PIN): enable = await cg.gpio_pin_expression(enable_pin) diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp index 91eb947a3e..042b8877e6 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp @@ -6,7 +6,6 @@ namespace esphome { namespace rpi_dpi_rgb { void RpiDpiRgb::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->reset_display_(); esp_lcd_rgb_panel_config_t config{}; config.flags.fb_in_psram = 1; @@ -23,7 +22,6 @@ void RpiDpiRgb::setup() { config.timings.flags.pclk_active_neg = this->pclk_inverted_; config.timings.pclk_hz = this->pclk_frequency_; config.clk_src = LCD_CLK_SRC_PLL160M; - config.psram_trans_align = 64; size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); for (size_t i = 0; i != data_pin_count; i++) { config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); @@ -42,7 +40,6 @@ void RpiDpiRgb::setup() { } ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_)); ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_)); - ESP_LOGCONFIG(TAG, "RPI_DPI_RGB setup complete"); } void RpiDpiRgb::loop() { if (this->handle_ != nullptr) 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/scd30.cpp b/esphome/components/scd30/scd30.cpp index 8561732d8b..3c2c06fd68 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -26,8 +26,6 @@ static const uint16_t SCD30_CMD_TEMPERATURE_OFFSET = 0x5403; static const uint16_t SCD30_CMD_SOFT_RESET = 0xD304; void SCD30Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - #ifdef USE_ESP8266 Wire.setClockStretchLimit(150000); #endif 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/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index 06db70e3f3..a265386cc2 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -27,7 +27,6 @@ static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f; static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f; void SCD4XComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { this->status_clear_error(); 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/display.py b/esphome/components/sdl/display.py index ae8b0fd43a..78c180aa65 100644 --- a/esphome/components/sdl/display.py +++ b/esphome/components/sdl/display.py @@ -36,7 +36,9 @@ def get_sdl_options(value): if value != "": return value try: - return subprocess.check_output(["sdl2-config", "--cflags", "--libs"]).decode() + return subprocess.check_output( + ["sdl2-config", "--cflags", "--libs"], close_fds=False + ).decode() except Exception as e: raise cv.Invalid("Unable to run sdl2-config - have you installed sdl2?") from e diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index 5ad18f6311..f235e4e68c 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -6,7 +6,6 @@ namespace esphome { namespace sdl { void Sdl::setup() { - ESP_LOGD(TAG, "Starting setup"); SDL_Init(SDL_INIT_VIDEO); this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_, this->window_options_); @@ -15,7 +14,6 @@ void Sdl::setup() { this->texture_ = SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STATIC, this->width_, this->height_); SDL_SetTextureBlendMode(this->texture_, SDL_BLENDMODE_BLEND); - ESP_LOGD(TAG, "Setup Complete"); } void Sdl::update() { this->do_update_(); diff --git a/esphome/components/sdp3x/sdp3x.cpp b/esphome/components/sdp3x/sdp3x.cpp index 58aefe09d7..d4ab04e7cd 100644 --- a/esphome/components/sdp3x/sdp3x.cpp +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -17,8 +17,6 @@ static const uint16_t SDP3X_STOP_MEAS = 0x3FF9; void SDP3XComponent::update() { this->read_pressure_(); } void SDP3XComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - if (!this->write_command(SDP3X_STOP_MEAS)) { ESP_LOGW(TAG, "Stop failed"); // This sometimes fails for no good reason } diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 8683d6cad7..76523ce5c0 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -62,7 +62,6 @@ void MR24HPC1Component::dump_config() { // Initialisation functions void MR24HPC1Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->check_uart_settings(115200); if (this->custom_mode_number_ != nullptr) { @@ -91,7 +90,6 @@ void MR24HPC1Component::setup() { memset(this->sg_frame_buf_, 0, FRAME_BUF_MAX_SIZE); this->set_interval(8000, [this]() { this->update_(); }); - ESP_LOGCONFIG(TAG, "Set up MR24HPC1 complete"); } // Timed polling of radar data diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index e40cd9c0c7..dea7976578 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -31,7 +31,6 @@ void MR60FDA2Component::dump_config() { // Initialisation functions void MR60FDA2Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->check_uart_settings(115200); this->current_frame_locate_ = LOCATE_FRAME_HEADER; @@ -43,8 +42,6 @@ void MR60FDA2Component::setup() { memset(this->current_frame_buf_, 0, FRAME_BUF_MAX_SIZE); memset(this->current_data_buf_, 0, DATA_BUF_MAX_SIZE); - - ESP_LOGCONFIG(TAG, "Set up MR60FDA2 complete"); } // main loop diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index ed1f6c020d..756e98c906 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -118,15 +118,14 @@ async def register_select(var, config, *, options: list[str]): await setup_select_core_(var, config, options=options) -async def new_select(config, *, options: list[str]): - var = cg.new_Pvariable(config[CONF_ID]) +async def new_select(config, *args, options: list[str]): + var = cg.new_Pvariable(config[CONF_ID], *args) await register_select(var, config, options=options) return var @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_SELECT") cg.add_global(select_ns.using) diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index 89da30c405..a8cd4290c8 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -5,7 +5,7 @@ namespace select { void SelectTraits::set_options(std::vector options) { this->options_ = std::move(options); } -std::vector SelectTraits::get_options() const { return this->options_; } +const std::vector &SelectTraits::get_options() const { return this->options_; } } // namespace select } // namespace esphome diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index ccf23dc6d0..128066dd6b 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -9,7 +9,7 @@ namespace select { class SelectTraits { public: void set_options(std::vector options); - std::vector get_options() const; + const std::vector &get_options() const; protected: std::vector options_; diff --git a/esphome/components/sen0321/sen0321.cpp b/esphome/components/sen0321/sen0321.cpp index c727dda0b1..6a5931272d 100644 --- a/esphome/components/sen0321/sen0321.cpp +++ b/esphome/components/sen0321/sen0321.cpp @@ -8,7 +8,6 @@ namespace sen0321_sensor { static const char *const TAG = "sen0321_sensor.sensor"; void Sen0321Sensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (!this->write_byte(SENSOR_MODE_REGISTER, SENSOR_MODE_AUTO)) { ESP_LOGW(TAG, "Error setting measurement mode."); this->mark_failed(); diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index c7fd997b0c..0f27ec1b10 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -30,8 +30,6 @@ static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor void SEN5XComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { // Check if measurement is ready before reading the value diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ea74361d51..277718e46c 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, @@ -254,6 +256,7 @@ OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter) ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) +ThrottleWithPriorityFilter = sensor_ns.class_("ThrottleWithPriorityFilter", Filter) TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) @@ -330,6 +333,7 @@ def sensor_schema( device_class: str = cv.UNDEFINED, state_class: str = cv.UNDEFINED, entity_category: str = cv.UNDEFINED, + filters: list = cv.UNDEFINED, ) -> cv.Schema: schema = {} @@ -344,6 +348,7 @@ def sensor_schema( (CONF_DEVICE_CLASS, device_class, validate_device_class), (CONF_STATE_CLASS, state_class, validate_state_class), (CONF_ENTITY_CATEGORY, entity_category, sensor_entity_category), + (CONF_FILTERS, filters, validate_filters), ]: if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator @@ -591,6 +596,29 @@ async def throttle_filter_to_code(config, filter_id): return cg.new_Pvariable(filter_id, config) +THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value( + { + cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds, + cv.Optional(CONF_VALUE, default="nan"): cv.Any( + cv.templatable(cv.float_), [cv.templatable(cv.float_)] + ), + }, + key=CONF_TIMEOUT, +) + + +@FILTER_REGISTRY.register( + "throttle_with_priority", + ThrottleWithPriorityFilter, + THROTTLE_WITH_PRIORITY_SCHEMA, +) +async def throttle_with_priority_filter_to_code(config, filter_id): + if not isinstance(config[CONF_VALUE], list): + config[CONF_VALUE] = [config[CONF_VALUE]] + template_ = [await cg.templatable(x, [], float) for x in config[CONF_VALUE]] + return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) + + @FILTER_REGISTRY.register( "heartbeat", HeartbeatFilter, cv.positive_time_period_milliseconds ) @@ -603,7 +631,9 @@ async def heartbeat_filter_to_code(config, filter_id): TIMEOUT_SCHEMA = cv.maybe_simple_value( { cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds, - cv.Optional(CONF_VALUE, default="nan"): cv.templatable(cv.float_), + cv.Optional(CONF_VALUE, default="nan"): cv.Any( + "last", cv.templatable(cv.float_) + ), }, key=CONF_TIMEOUT, ) @@ -611,8 +641,11 @@ TIMEOUT_SCHEMA = cv.maybe_simple_value( @FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA) async def timeout_filter_to_code(config, filter_id): - template_ = await cg.templatable(config[CONF_VALUE], [], float) - var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) + if config[CONF_VALUE] == "last": + var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT]) + else: + template_ = await cg.templatable(config[CONF_VALUE], [], float) + var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) await cg.register_component(var, {}) return var @@ -1111,5 +1144,4 @@ def _lstsq(a, b): @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_SENSOR") cg.add_global(sensor_ns.using) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index dd8635f0c0..3241ae28af 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -1,5 +1,6 @@ #include "filter.h" #include +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "sensor.h" @@ -50,6 +51,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); @@ -223,7 +225,7 @@ optional SlidingWindowMovingAverageFilter::new_value(float value) { // ExponentialMovingAverageFilter ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at) - : send_every_(send_every), send_at_(send_every - send_first_at), alpha_(alpha) {} + : alpha_(alpha), send_every_(send_every), send_at_(send_every - send_first_at) {} optional ExponentialMovingAverageFilter::new_value(float value) { if (!std::isnan(value)) { if (this->first_value_) { @@ -323,7 +325,7 @@ optional FilterOutValueFilter::new_value(float value) { // ThrottleFilter ThrottleFilter::ThrottleFilter(uint32_t min_time_between_inputs) : min_time_between_inputs_(min_time_between_inputs) {} optional ThrottleFilter::new_value(float value) { - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_) { this->last_input_ = now; return value; @@ -331,21 +333,53 @@ optional ThrottleFilter::new_value(float value) { return {}; } +// ThrottleWithPriorityFilter +ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, + std::vector> prioritized_values) + : min_time_between_inputs_(min_time_between_inputs), prioritized_values_(std::move(prioritized_values)) {} + +optional ThrottleWithPriorityFilter::new_value(float value) { + bool is_prioritized_value = false; + int8_t accuracy = this->parent_->get_accuracy_decimals(); + float accuracy_mult = powf(10.0f, accuracy); + const uint32_t now = App.get_loop_component_start_time(); + // First, determine if the new value is one of the prioritized values + for (auto prioritized_value : this->prioritized_values_) { + if (std::isnan(prioritized_value.value())) { + if (std::isnan(value)) { + is_prioritized_value = true; + break; + } + continue; + } + float rounded_prioritized_value = roundf(accuracy_mult * prioritized_value.value()); + float rounded_value = roundf(accuracy_mult * value); + if (rounded_prioritized_value == rounded_value) { + is_prioritized_value = true; + break; + } + } + // Finally, determine if the new value should be throttled and pass it through if not + if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ || is_prioritized_value) { + this->last_input_ = now; + return value; + } + return {}; +} + // DeltaFilter DeltaFilter::DeltaFilter(float delta, bool percentage_mode) - : delta_(delta), current_delta_(delta), percentage_mode_(percentage_mode), last_value_(NAN) {} + : delta_(delta), current_delta_(delta), last_value_(NAN), percentage_mode_(percentage_mode) {} optional DeltaFilter::new_value(float value) { if (std::isnan(value)) { if (std::isnan(this->last_value_)) { return {}; } else { - if (this->percentage_mode_) { - this->current_delta_ = fabsf(value * this->delta_); - } return this->last_value_ = value; } } - if (std::isnan(this->last_value_) || fabsf(value - this->last_value_) >= this->current_delta_) { + float diff = fabsf(value - this->last_value_); + if (std::isnan(this->last_value_) || (diff > 0.0f && diff >= this->current_delta_)) { if (this->percentage_mode_) { this->current_delta_ = fabsf(value * this->delta_); } @@ -383,12 +417,17 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { // TimeoutFilter optional TimeoutFilter::new_value(float value) { - this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value()); }); + if (this->value_.has_value()) { + this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value().value()); }); + } else { + this->set_timeout("timeout", this->time_period_, [this, value]() { this->output(value); }); + } return value; } -TimeoutFilter::TimeoutFilter(uint32_t time_period, TemplatableValue new_value) - : time_period_(time_period), value_(std::move(new_value)) {} +TimeoutFilter::TimeoutFilter(uint32_t time_period) : time_period_(time_period) {} +TimeoutFilter::TimeoutFilter(uint32_t time_period, const TemplatableValue &new_value) + : time_period_(time_period), value_(new_value) {} float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } // DebounceFilter diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 94fec8208b..49d83e5b4b 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -221,11 +221,11 @@ class ExponentialMovingAverageFilter : public Filter { void set_alpha(float alpha); protected: - bool first_value_{true}; float accumulator_{NAN}; + float alpha_; size_t send_every_; size_t send_at_; - float alpha_; + bool first_value_{true}; }; /** Simple throttle average filter. @@ -243,9 +243,9 @@ class ThrottleAverageFilter : public Filter, public Component { float get_setup_priority() const override; protected: - uint32_t time_period_; float sum_{0.0f}; unsigned int n_{0}; + uint32_t time_period_; bool have_nan_{false}; }; @@ -314,9 +314,24 @@ class ThrottleFilter : public Filter { uint32_t min_time_between_inputs_; }; +/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`. +class ThrottleWithPriorityFilter : public Filter { + public: + explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, + std::vector> prioritized_values); + + optional new_value(float value) override; + + protected: + uint32_t last_input_{0}; + uint32_t min_time_between_inputs_; + std::vector> prioritized_values_; +}; + class TimeoutFilter : public Filter, public Component { public: - explicit TimeoutFilter(uint32_t time_period, TemplatableValue new_value); + explicit TimeoutFilter(uint32_t time_period); + explicit TimeoutFilter(uint32_t time_period, const TemplatableValue &new_value); optional new_value(float value) override; @@ -324,7 +339,7 @@ class TimeoutFilter : public Filter, public Component { protected: uint32_t time_period_; - TemplatableValue value_; + optional> value_; }; class DebounceFilter : public Filter, public Component { @@ -364,8 +379,8 @@ class DeltaFilter : public Filter { protected: float delta_; float current_delta_; - bool percentage_mode_; float last_value_{NAN}; + bool percentage_mode_; }; class OrFilter : public Filter { @@ -387,8 +402,8 @@ class OrFilter : public Filter { }; std::vector filters_; - bool has_value_{false}; PhiNode phi_; + bool has_value_{false}; }; class CalibrateLinearFilter : public Filter { 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/sfa30/sfa30.cpp b/esphome/components/sfa30/sfa30.cpp index c521b3aa02..99709d5fbb 100644 --- a/esphome/components/sfa30/sfa30.cpp +++ b/esphome/components/sfa30/sfa30.cpp @@ -11,8 +11,6 @@ static const uint16_t SFA30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0006; static const uint16_t SFA30_CMD_READ_MEASUREMENT = 0x0327; void SFA30Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Serial Number identification uint16_t raw_device_marking[16]; if (!this->get_register(SFA30_CMD_GET_DEVICE_MARKING, raw_device_marking, 16, 5)) { @@ -34,8 +32,6 @@ void SFA30Component::setup() { this->mark_failed(); return; } - - ESP_LOGD(TAG, "Sensor initialized"); } void SFA30Component::dump_config() { diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 0c7f25b699..42baff6d23 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -33,8 +33,6 @@ const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 3600; const uint32_t MAXIMUM_STORAGE_DIFF = 50; void SGP30Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Serial Number identification uint16_t raw_serial_number[3]; if (!this->get_register(SGP30_CMD_GET_SERIAL_ID, raw_serial_number, 3)) { diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index 4f29248881..7c6fe580b2 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -139,7 +139,7 @@ async def to_code(config): ) ) cg.add_library( - None, + "Sensirion Gas Index Algorithm", None, "https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1", ) diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index bd84ae97f3..da52993a87 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -9,8 +9,6 @@ namespace sgp4x { static const char *const TAG = "sgp4x"; void SGP4xComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Serial Number identification uint16_t raw_serial_number[3]; if (!this->get_register(SGP4X_CMD_GET_SERIAL_ID, raw_serial_number, 3, 1)) { diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index 6b4ab13c48..b336bbcb65 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -101,8 +101,6 @@ void ShellyDimmer::setup() { this->pin_nrst_->setup(); this->pin_boot0_->setup(); - ESP_LOGI(TAG, "Initializing"); - this->handle_firmware(); this->send_settings_(); diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index 9dc866ddc3..79f1674020 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -25,7 +25,6 @@ static const uint16_t SHT3XD_COMMAND_POLLING_H = 0x2400; static const uint16_t SHT3XD_COMMAND_FETCH_DATA = 0xE000; void SHT3XDComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint16_t raw_serial_number[2]; if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER_CLOCK_STRETCHING, raw_serial_number, 2)) { this->error_code_ = READ_SERIAL_STRETCHED_FAILED; @@ -61,7 +60,6 @@ void SHT3XDComponent::dump_config() { ESP_LOGE(TAG, " Communication with SHT3xD failed!"); return; } - ESP_LOGD(TAG, " Setup successful"); ESP_LOGD(TAG, " Serial Number: 0x%08" PRIX32, this->serial_number_); ESP_LOGD(TAG, " Heater Enabled: %s", this->heater_enabled_ ? "true" : "false"); diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 944b13023e..637c8c1a9d 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -18,8 +18,6 @@ void SHT4XComponent::start_heater_() { } void SHT4XComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - auto err = this->write(nullptr, 0); if (err != i2c::ERROR_OK) { this->mark_failed(); diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index 5420119bd6..d532bd7f44 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -25,7 +25,6 @@ inline const char *to_string(SHTCXType type) { } void SHTCXComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->wake_up(); this->soft_reset(); diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index 2ca9127d3f..c48a3c63c4 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -171,8 +171,7 @@ async def sim800l_dial_to_code(config, action_id, template_arg, args): ) async def sim800l_connect_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) SIM800L_SEND_USSD_SCHEMA = cv.Schema( @@ -201,5 +200,4 @@ async def sim800l_send_ussd_to_code(config, action_id, template_arg, args): ) async def sim800l_disconnect_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/sm16716/sm16716.cpp b/esphome/components/sm16716/sm16716.cpp index b25f935eba..aa33b7b679 100644 --- a/esphome/components/sm16716/sm16716.cpp +++ b/esphome/components/sm16716/sm16716.cpp @@ -7,7 +7,6 @@ namespace sm16716 { static const char *const TAG = "sm16716"; void SM16716::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->data_pin_->setup(); this->data_pin_->digital_write(false); this->clock_pin_->setup(); diff --git a/esphome/components/sm2135/sm2135.cpp b/esphome/components/sm2135/sm2135.cpp index cd647ef3b9..e55f836929 100644 --- a/esphome/components/sm2135/sm2135.cpp +++ b/esphome/components/sm2135/sm2135.cpp @@ -20,7 +20,6 @@ static const uint8_t SM2135_RGB = 0x00; // RGB channel static const uint8_t SM2135_CW = 0x80; // CW channel (Chip default) void SM2135::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->data_pin_->setup(); this->data_pin_->digital_write(false); this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); diff --git a/esphome/components/sm2235/sm2235.cpp b/esphome/components/sm2235/sm2235.cpp index e9f84773e2..820fcb521a 100644 --- a/esphome/components/sm2235/sm2235.cpp +++ b/esphome/components/sm2235/sm2235.cpp @@ -7,7 +7,6 @@ namespace sm2235 { static const char *const TAG = "sm2235"; void SM2235::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->data_pin_->setup(); this->data_pin_->digital_write(true); this->clock_pin_->setup(); diff --git a/esphome/components/sm2335/sm2335.cpp b/esphome/components/sm2335/sm2335.cpp index 99b722a639..0580a782f5 100644 --- a/esphome/components/sm2335/sm2335.cpp +++ b/esphome/components/sm2335/sm2335.cpp @@ -7,7 +7,6 @@ namespace sm2335 { static const char *const TAG = "sm2335"; void SM2335::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->data_pin_->setup(); this->data_pin_->digital_write(true); this->clock_pin_->setup(); diff --git a/esphome/components/sn74hc165/sn74hc165.cpp b/esphome/components/sn74hc165/sn74hc165.cpp index 69e0df5785..416d9db293 100644 --- a/esphome/components/sn74hc165/sn74hc165.cpp +++ b/esphome/components/sn74hc165/sn74hc165.cpp @@ -7,7 +7,6 @@ namespace sn74hc165 { static const char *const TAG = "sn74hc165"; void SN74HC165Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // initialize pins this->clock_pin_->setup(); this->data_pin_->setup(); diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index d8e33eec22..fc47a6dc5e 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -8,7 +8,6 @@ namespace sn74hc595 { static const char *const TAG = "sn74hc595"; void SN74HC595Component::pre_setup_() { - ESP_LOGCONFIG(TAG, "Running setup"); if (this->have_oe_pin_) { // disable output this->oe_pin_->setup(); this->oe_pin_->digital_write(true); diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index d5839c1a2b..ccd9af3153 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -15,7 +15,6 @@ namespace sntp { static const char *const TAG = "sntp"; void SNTPComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); #if defined(USE_ESP32) if (esp_sntp_enabled()) { esp_sntp_stop(); diff --git a/esphome/components/sound_level/sensor.py b/esphome/components/sound_level/sensor.py index 292efadab8..8ca0feccc0 100644 --- a/esphome/components/sound_level/sensor.py +++ b/esphome/components/sound_level/sensor.py @@ -12,7 +12,7 @@ from esphome.const import ( UNIT_DECIBEL, ) -AUTOLOAD = ["audio"] +AUTO_LOAD = ["audio"] CODEOWNERS = ["@kahrendt"] DEPENDENCIES = ["microphone"] diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index dc2dae2ef1..3ae7b980d3 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -155,8 +155,7 @@ def _read_audio_file_and_type(file_config): import puremagic file_type: str = puremagic.from_string(data) - if file_type.startswith("."): - file_type = file_type[1:] + file_type = file_type.removeprefix(".") media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] if file_type in ("wav"): @@ -204,13 +203,14 @@ def _validate_pipeline(config): def _validate_repeated_speaker(config): - if (announcement_config := config.get(CONF_ANNOUNCEMENT_PIPELINE)) and ( - media_config := config.get(CONF_MEDIA_PIPELINE) + if ( + (announcement_config := config.get(CONF_ANNOUNCEMENT_PIPELINE)) + and (media_config := config.get(CONF_MEDIA_PIPELINE)) + and announcement_config[CONF_SPEAKER] == media_config[CONF_SPEAKER] ): - if announcement_config[CONF_SPEAKER] == media_config[CONF_SPEAKER]: - raise cv.Invalid( - "The announcement and media pipelines cannot use the same speaker. Use the `mixer` speaker component to create two source speakers." - ) + raise cv.Invalid( + "The announcement and media pipelines cannot use the same speaker. Use the `mixer` speaker component to create two source speakers." + ) return config @@ -315,31 +315,19 @@ async def to_code(config): cg.add_define("USE_AUDIO_FLAC_SUPPORT", True) cg.add_define("USE_AUDIO_MP3_SUPPORT", True) - # Wifi settings based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM", 16) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM", 512) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_TX_BUFFER", True) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_TX_BUFFER_TYPE", 0) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM", 8) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_CACHE_TX_BUFFER_NUM", 32) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED", True) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_TX_BA_WIN", 16) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED", True) - esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_RX_BA_WIN", 32) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_MAXRTX", 12) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_SYNMAXRTX", 6) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_MSS", 1436) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_MSL", 60000) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_SND_BUF_DEFAULT", 65535) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_WND_DEFAULT", 512000) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_RECVMBOX_SIZE", 512) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_QUEUE_OOSEQ", True) - esp32.add_idf_sdkconfig_option("CONFIG_TCP_OVERSIZE_MSS", True) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RCV_SCALE", 3) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512) + # Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32 + esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16) + esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64) + esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64) + esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True) + esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32) + esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True) + esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32) + + esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534) + esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534) + esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64) + esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64) # Allocate wifi buffers in PSRAM esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 8811ea1644..dc8572ae43 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -259,13 +259,10 @@ esp_err_t AudioPipeline::allocate_communications_() { esp_err_t AudioPipeline::start_tasks_() { if (this->read_task_handle_ == nullptr) { if (this->read_task_stack_buffer_ == nullptr) { - if (this->task_stack_in_psram_) { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_EXTERNAL); - this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE); - } else { - RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); - this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE); - } + // Reader task uses the AudioReader class which uses esp_http_client. This crashes on IDF 5.4 if the task stack is + // in PSRAM. As a workaround, always allocate the read task in internal memory. + RAMAllocator stack_allocator(RAMAllocator::ALLOC_INTERNAL); + this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE); } if (this->read_task_stack_buffer_ == nullptr) { diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 58bfc3f411..a436bc6dab 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 @@ -114,9 +115,7 @@ def get_target_platform(): def get_target_variant(): - return ( - CORE.data[KEY_ESP32][KEY_VARIANT] if KEY_VARIANT in CORE.data[KEY_ESP32] else "" - ) + return CORE.data[KEY_ESP32].get(KEY_VARIANT, "") # Get a list of available hardware interfaces based on target and variant. @@ -139,6 +138,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()): @@ -191,9 +211,7 @@ def validate_hw_pins(spi, index=-1): return False if sdo_pin_no not in pin_set[CONF_MOSI_PIN]: return False - if sdi_pin_no not in pin_set[CONF_MISO_PIN]: - return False - return True + return sdi_pin_no in pin_set[CONF_MISO_PIN] return False @@ -274,9 +292,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 +326,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/spi/spi.cpp b/esphome/components/spi/spi.cpp index 805a774ceb..00e9845a03 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -37,8 +37,6 @@ void SPIComponent::unregister_device(SPIClient *device) { } void SPIComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - if (this->sdo_pin_ == nullptr) this->sdo_pin_ = NullPin::NULL_PIN; if (this->sdi_pin_ == nullptr) diff --git a/esphome/components/spi_device/spi_device.cpp b/esphome/components/spi_device/spi_device.cpp index 872b3054e6..dbfbc9eccb 100644 --- a/esphome/components/spi_device/spi_device.cpp +++ b/esphome/components/spi_device/spi_device.cpp @@ -8,10 +8,7 @@ namespace spi_device { static const char *const TAG = "spi_device"; -void SPIDeviceComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->spi_setup(); -} +void SPIDeviceComponent::setup() { this->spi_setup(); } void SPIDeviceComponent::dump_config() { ESP_LOGCONFIG(TAG, "SPIDevice"); diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 3c94d97739..2dccb6896a 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -130,11 +130,11 @@ def validate_sprinkler(config): if ( CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY in sprinkler_controller and CONF_VALVE_OPEN_DELAY not in sprinkler_controller + and sprinkler_controller[CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY] ): - if sprinkler_controller[CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY]: - raise cv.Invalid( - f"{CONF_VALVE_OPEN_DELAY} must be defined when {CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY} is enabled" - ) + raise cv.Invalid( + f"{CONF_VALVE_OPEN_DELAY} must be defined when {CONF_PUMP_SWITCH_OFF_DURING_VALVE_OPEN_DELAY} is enabled" + ) if ( CONF_REPEAT in sprinkler_controller diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index c0df539867..272acc78f2 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -22,7 +22,6 @@ static const size_t SERIAL_NUMBER_LENGTH = 8; static const uint8_t MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR = 5; void SPS30Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->write_command(SPS30_CMD_SOFT_RESET); /// Deferred Sensor initialization this->set_timeout(500, [this]() { diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index ab2c7a5496..9d397e396b 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, @@ -43,14 +42,15 @@ SSD1306_MODEL = cv.enum(MODELS, upper=True, space="_") def _validate(value): model = value[CONF_MODEL] - if model not in ("SSD1305_128X32", "SSD1305_128X64"): - # Contrast is default value (1.0) while brightness is not - # Indicates user is using old `brightness` option - if value[CONF_BRIGHTNESS] != 1.0 and value[CONF_CONTRAST] == 1.0: - raise cv.Invalid( - "SSD1306/SH1106 no longer accepts brightness option, " - 'please use "contrast" instead.' - ) + if ( + model not in ("SSD1305_128X32", "SSD1305_128X64") + and value[CONF_BRIGHTNESS] != 1.0 + and value[CONF_CONTRAST] == 1.0 + ): + raise cv.Invalid( + "SSD1306/SH1106 no longer accepts brightness option, " + 'please use "contrast" instead.' + ) return value 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/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index f9a2609948..8e490834bc 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -7,7 +7,6 @@ namespace ssd1306_i2c { static const char *const TAG = "ssd1306_i2c"; void I2CSSD1306::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->init_reset_(); auto err = this->write(nullptr, 0); diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index 249e6593ae..d93742c0e5 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -8,7 +8,6 @@ namespace ssd1306_spi { static const char *const TAG = "ssd1306_spi"; void SPISSD1306::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->dc_pin_->setup(); // OUTPUT diff --git a/esphome/components/ssd1322_spi/ssd1322_spi.cpp b/esphome/components/ssd1322_spi/ssd1322_spi.cpp index fb2d8afe1c..6a8918353b 100644 --- a/esphome/components/ssd1322_spi/ssd1322_spi.cpp +++ b/esphome/components/ssd1322_spi/ssd1322_spi.cpp @@ -8,7 +8,6 @@ namespace ssd1322_spi { static const char *const TAG = "ssd1322_spi"; void SPISSD1322::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->dc_pin_->setup(); // OUTPUT if (this->cs_) diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp index d2a365326f..3c9dfd3324 100644 --- a/esphome/components/ssd1325_spi/ssd1325_spi.cpp +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -8,7 +8,6 @@ namespace ssd1325_spi { static const char *const TAG = "ssd1325_spi"; void SPISSD1325::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->dc_pin_->setup(); // OUTPUT if (this->cs_) diff --git a/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp b/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp index 4e1c5e4ea0..3597a38c44 100644 --- a/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp +++ b/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp @@ -7,7 +7,6 @@ namespace ssd1327_i2c { static const char *const TAG = "ssd1327_i2c"; void I2CSSD1327::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->init_reset_(); auto err = this->write(nullptr, 0); diff --git a/esphome/components/ssd1327_spi/ssd1327_spi.cpp b/esphome/components/ssd1327_spi/ssd1327_spi.cpp index a5eaf252c4..c26238ae19 100644 --- a/esphome/components/ssd1327_spi/ssd1327_spi.cpp +++ b/esphome/components/ssd1327_spi/ssd1327_spi.cpp @@ -8,7 +8,6 @@ namespace ssd1327_spi { static const char *const TAG = "ssd1327_spi"; void SPISSD1327::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->dc_pin_->setup(); // OUTPUT if (this->cs_) diff --git a/esphome/components/ssd1331_spi/ssd1331_spi.cpp b/esphome/components/ssd1331_spi/ssd1331_spi.cpp index aeff2bbbfd..232822d192 100644 --- a/esphome/components/ssd1331_spi/ssd1331_spi.cpp +++ b/esphome/components/ssd1331_spi/ssd1331_spi.cpp @@ -8,7 +8,6 @@ namespace ssd1331_spi { static const char *const TAG = "ssd1331_spi"; void SPISSD1331::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->dc_pin_->setup(); // OUTPUT if (this->cs_) diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.cpp b/esphome/components/ssd1351_spi/ssd1351_spi.cpp index 5ae7c308d4..ffac07b82b 100644 --- a/esphome/components/ssd1351_spi/ssd1351_spi.cpp +++ b/esphome/components/ssd1351_spi/ssd1351_spi.cpp @@ -8,7 +8,6 @@ namespace ssd1351_spi { static const char *const TAG = "ssd1351_spi"; void SPISSD1351::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->dc_pin_->setup(); // OUTPUT if (this->cs_) diff --git a/esphome/components/st7567_i2c/st7567_i2c.cpp b/esphome/components/st7567_i2c/st7567_i2c.cpp index 0640d3be8d..4970367343 100644 --- a/esphome/components/st7567_i2c/st7567_i2c.cpp +++ b/esphome/components/st7567_i2c/st7567_i2c.cpp @@ -7,7 +7,6 @@ namespace st7567_i2c { static const char *const TAG = "st7567_i2c"; void I2CST7567::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->init_reset_(); auto err = this->write(nullptr, 0); diff --git a/esphome/components/st7567_spi/st7567_spi.cpp b/esphome/components/st7567_spi/st7567_spi.cpp index c5c5836200..813afcf682 100644 --- a/esphome/components/st7567_spi/st7567_spi.cpp +++ b/esphome/components/st7567_spi/st7567_spi.cpp @@ -7,7 +7,6 @@ namespace st7567_spi { static const char *const TAG = "st7567_spi"; void SPIST7567::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->dc_pin_->setup(); if (this->cs_) diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py index c6ad43c14c..497740b8d2 100644 --- a/esphome/components/st7701s/display.py +++ b/esphome/components/st7701s/display.py @@ -2,9 +2,17 @@ from esphome import pins import esphome.codegen as cg from esphome.components import display, spi from esphome.components.esp32 import const, only_on_variant -from esphome.components.rpi_dpi_rgb.display import ( +from esphome.components.mipi import ( + CONF_DE_PIN, + CONF_HSYNC_BACK_PORCH, + CONF_HSYNC_FRONT_PORCH, + CONF_HSYNC_PULSE_WIDTH, CONF_PCLK_FREQUENCY, CONF_PCLK_INVERTED, + CONF_PCLK_PIN, + CONF_VSYNC_BACK_PORCH, + CONF_VSYNC_FRONT_PORCH, + CONF_VSYNC_PULSE_WIDTH, ) import esphome.config_validation as cv from esphome.const import ( @@ -36,16 +44,6 @@ from esphome.core import TimePeriod from .init_sequences import ST7701S_INITS, cmd -CONF_DE_PIN = "de_pin" -CONF_PCLK_PIN = "pclk_pin" - -CONF_HSYNC_PULSE_WIDTH = "hsync_pulse_width" -CONF_HSYNC_BACK_PORCH = "hsync_back_porch" -CONF_HSYNC_FRONT_PORCH = "hsync_front_porch" -CONF_VSYNC_PULSE_WIDTH = "vsync_pulse_width" -CONF_VSYNC_BACK_PORCH = "vsync_back_porch" -CONF_VSYNC_FRONT_PORCH = "vsync_front_porch" - DEPENDENCIES = ["spi", "esp32"] st7701s_ns = cg.esphome_ns.namespace("st7701s") @@ -191,7 +189,6 @@ async def to_code(config): cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) - index = 0 dpins = [] if CONF_RED in config[CONF_DATA_PINS]: red_pins = config[CONF_DATA_PINS][CONF_RED] @@ -209,10 +206,9 @@ async def to_code(config): dpins = dpins[8:16] + dpins[0:8] else: dpins = config[CONF_DATA_PINS] - for pin in dpins: + for index, pin in enumerate(dpins): data_pin = await cg.gpio_pin_expression(pin) cg.add(var.add_data_pin(data_pin, index)) - index += 1 if dc_pin := config.get(CONF_DC_PIN): dc = await cg.gpio_pin_expression(dc_pin) diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index 46509a7f9f..6314c99fb0 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -6,7 +6,6 @@ namespace esphome { namespace st7701s { void ST7701S::setup() { - esph_log_config(TAG, "Setting up ST7701S"); this->spi_setup(); this->write_init_sequence_(); @@ -25,7 +24,6 @@ void ST7701S::setup() { config.timings.flags.pclk_active_neg = this->pclk_inverted_; config.timings.pclk_hz = this->pclk_frequency_; config.clk_src = LCD_CLK_SRC_PLL160M; - config.psram_trans_align = 64; size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); for (size_t i = 0; i != data_pin_count; i++) { config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); @@ -42,7 +40,6 @@ void ST7701S::setup() { if (err != ESP_OK) { esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err)); } - esph_log_config(TAG, "ST7701S setup complete"); } void ST7701S::loop() { diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index 9c9c0a3df5..160ba151f7 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -233,7 +233,6 @@ ST7735::ST7735(ST7735Model model, int width, int height, int colstart, int rowst height_(height) {} void ST7735::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->spi_setup(); this->dc_pin_->setup(); // OUTPUT diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index 1f3cd50d6c..44f2293ac4 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -8,7 +8,6 @@ static const char *const TAG = "st7789v"; static const size_t TEMP_BUFFER_SIZE = 128; void ST7789V::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); #ifdef USE_POWER_SUPPLY this->power_.request(); // the PowerSupply component takes care of post turn-on delay diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp index 54ac6d2efd..c7ce7140e3 100644 --- a/esphome/components/st7920/st7920.cpp +++ b/esphome/components/st7920/st7920.cpp @@ -32,7 +32,6 @@ static const uint8_t LCD_LINE2 = 0x88; static const uint8_t LCD_LINE3 = 0x98; void ST7920::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->dump_config(); this->spi_setup(); this->init_internal_(this->get_buffer_length_()); diff --git a/esphome/components/status_led/light/status_led_light.cpp b/esphome/components/status_led/light/status_led_light.cpp index dc4820f6da..ec7bf2dae1 100644 --- a/esphome/components/status_led/light/status_led_light.cpp +++ b/esphome/components/status_led/light/status_led_light.cpp @@ -53,8 +53,6 @@ void StatusLEDLightOutput::write_state(light::LightState *state) { } void StatusLEDLightOutput::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - if (this->pin_ != nullptr) { this->pin_->setup(); this->pin_->digital_write(false); diff --git a/esphome/components/status_led/status_led.cpp b/esphome/components/status_led/status_led.cpp index a17d4398fd..344c1e3070 100644 --- a/esphome/components/status_led/status_led.cpp +++ b/esphome/components/status_led/status_led.cpp @@ -11,7 +11,6 @@ StatusLED *global_status_led = nullptr; // NOLINT(cppcoreguidelines-avoid-non-c StatusLED::StatusLED(GPIOPin *pin) : pin_(pin) { global_status_led = this; } void StatusLED::pre_setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->pin_->setup(); this->pin_->digital_write(false); } diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 66fe88c6c4..c234388e7e 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -27,8 +27,7 @@ SetDecelerationAction = stepper_ns.class_("SetDecelerationAction", automation.Ac def validate_acceleration(value): value = cv.string(value) for suffix in ("steps/s^2", "steps/s*s", "steps/s/s", "steps/ss", "steps/(s*s)"): - if value.endswith(suffix): - value = value[: -len(suffix)] + value = value.removesuffix(suffix) if value == "inf": return 1e6 @@ -48,8 +47,7 @@ def validate_acceleration(value): def validate_speed(value): value = cv.string(value) for suffix in ("steps/s", "steps/s"): - if value.endswith(suffix): - value = value[: -len(suffix)] + value = value.removesuffix(suffix) if value == "inf": return 1e6 diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp index 29aac24e90..eee2aca73e 100644 --- a/esphome/components/sts3x/sts3x.cpp +++ b/esphome/components/sts3x/sts3x.cpp @@ -18,7 +18,6 @@ static const uint16_t STS3X_COMMAND_HEATER_DISABLE = 0x3066; static const uint16_t STS3X_COMMAND_FETCH_DATA = 0xE000; void STS3XComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (!this->write_command(STS3X_COMMAND_READ_SERIAL_NUMBER)) { this->mark_failed(); return; diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index e07c2e3859..a96f56a045 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__) @@ -54,15 +49,14 @@ def _expand_jinja(value, orig_value, path, jinja, ignore_missing): try: # Invoke the jinja engine to evaluate the expression. value, err = jinja.expand(value) - if err is not None: - if not ignore_missing and "password" not in path: - _LOGGER.warning( - "Found '%s' (see %s) which looks like an expression," - " but could not resolve all the variables: %s", - value, - "->".join(str(x) for x in path), - err.message, - ) + if err is not None and not ignore_missing and "password" not in path: + _LOGGER.warning( + "Found '%s' (see %s) which looks like an expression," + " but could not resolve all the variables: %s", + value, + "->".join(str(x) for x in path), + err.message, + ) except ( TemplateError, TemplateRuntimeError, diff --git a/esphome/components/substitutions/jinja.py b/esphome/components/substitutions/jinja.py index 9ecdbab844..c6e40a668d 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 @@ -24,6 +25,24 @@ def has_jinja(st): return detect_jinja_re.search(st) is not None +# SAFE_GLOBAL_FUNCTIONS defines a allowlist of built-in functions that are considered safe to expose +# in Jinja templates or other sandboxed evaluation contexts. Only functions that do not allow +# arbitrary code execution, file access, or other security risks are included. +# +# The following functions are considered safe: +# - ord: Converts a character to its Unicode code point integer. +# - chr: Converts an integer to its corresponding Unicode character. +# - len: Returns the length of a sequence or collection. +# +# These functions were chosen because they are pure, have no side effects, and do not provide access +# to the file system, environment, or other potentially sensitive resources. +SAFE_GLOBAL_FUNCTIONS = { + "ord": ord, + "chr": chr, + "len": len, +} + + class JinjaStr(str): """ Wraps a string containing an unresolved Jinja expression, @@ -65,7 +84,11 @@ class Jinja: self.env.add_extension("jinja2.ext.do") self.env.globals["math"] = math # Inject entire math module self.context_vars = {**context_vars} - self.env.globals = {**self.env.globals, **self.context_vars} + self.env.globals = { + **self.env.globals, + **self.context_vars, + **SAFE_GLOBAL_FUNCTIONS, + } def expand(self, content_str): """ diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index 5a2a39c427..c065a82958 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -1,3 +1,4 @@ +import contextlib import re from esphome import automation @@ -41,12 +42,10 @@ ELEVATION_MAP = { def elevation(value): if isinstance(value, str): - try: + with contextlib.suppress(cv.Invalid): value = ELEVATION_MAP[ cv.one_of(*ELEVATION_MAP, lower=True, space="_")(value) ] - except cv.Invalid: - pass value = cv.angle(value) return cv.float_range(min=-180, max=180)(value) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index c09675069f..f495dbc0b4 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -10,9 +10,11 @@ from esphome.const import ( CONF_ID, CONF_INVERTED, CONF_MQTT_ID, + CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_RESTORE_MODE, + CONF_STATE, CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_EMPTY, @@ -48,12 +50,16 @@ RESTORE_MODES = { } +ControlAction = switch_ns.class_("ControlAction", automation.Action) ToggleAction = switch_ns.class_("ToggleAction", automation.Action) TurnOffAction = switch_ns.class_("TurnOffAction", automation.Action) TurnOnAction = switch_ns.class_("TurnOnAction", automation.Action) SwitchPublishAction = switch_ns.class_("SwitchPublishAction", automation.Action) SwitchCondition = switch_ns.class_("SwitchCondition", Condition) +SwitchStateTrigger = switch_ns.class_( + "SwitchStateTrigger", automation.Trigger.template(bool) +) SwitchTurnOnTrigger = switch_ns.class_( "SwitchTurnOnTrigger", automation.Trigger.template() ) @@ -75,6 +81,11 @@ _SWITCH_SCHEMA = ( cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum( RESTORE_MODES, upper=True, space="_" ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchStateTrigger), + } + ), cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOnTrigger), @@ -138,6 +149,9 @@ async def setup_switch_core_(var, config): if (inverted := config.get(CONF_INVERTED)) is not None: cg.add(var.set_inverted(inverted)) + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(bool, "x")], conf) for conf in config.get(CONF_ON_TURN_ON, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) @@ -177,6 +191,23 @@ SWITCH_ACTION_SCHEMA = maybe_simple_id( cv.Required(CONF_ID): cv.use_id(Switch), } ) +SWITCH_CONTROL_ACTION_SCHEMA = automation.maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Switch), + cv.Required(CONF_STATE): cv.templatable(cv.boolean), + } +) + + +@automation.register_action( + "switch.control", ControlAction, SWITCH_CONTROL_ACTION_SCHEMA +) +async def switch_control_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_STATE], args, bool) + cg.add(var.set_state(template_)) + return var @automation.register_action("switch.toggle", ToggleAction, SWITCH_ACTION_SCHEMA) @@ -202,4 +233,3 @@ async def switch_is_off_to_code(config, condition_id, template_arg, args): @coroutine_with_priority(100.0) async def to_code(config): cg.add_global(switch_ns.using) - cg.add_define("USE_SWITCH") diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index 579daf4d24..b8cbc9b976 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -37,6 +37,23 @@ template class ToggleAction : public Action { Switch *switch_; }; +template class ControlAction : public Action { + public: + explicit ControlAction(Switch *a_switch) : switch_(a_switch) {} + + TEMPLATABLE_VALUE(bool, state) + + void play(Ts... x) override { + auto state = this->state_.optional_value(x...); + if (state.has_value()) { + this->switch_->control(*state); + } + } + + protected: + Switch *switch_; +}; + template class SwitchCondition : public Condition { public: SwitchCondition(Switch *parent, bool state) : parent_(parent), state_(state) {} @@ -47,6 +64,13 @@ template class SwitchCondition : public Condition { bool state_; }; +class SwitchStateTrigger : public Trigger { + public: + SwitchStateTrigger(Switch *a_switch) { + a_switch->add_on_state_callback([this](bool state) { this->trigger(state); }); + } +}; + class SwitchTurnOnTrigger : public Trigger<> { public: SwitchTurnOnTrigger(Switch *a_switch) { diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index c204895755..13c12c1213 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -8,6 +8,14 @@ static const char *const TAG = "switch"; Switch::Switch() : state(false) {} +void Switch::control(bool target_state) { + ESP_LOGV(TAG, "'%s' Control: %s", this->get_name().c_str(), ONOFF(target_state)); + if (target_state) { + this->turn_on(); + } else { + this->turn_off(); + } +} void Switch::turn_on() { ESP_LOGD(TAG, "'%s' Turning ON.", this->get_name().c_str()); this->write_state(!this->inverted_); diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index b999296564..6371e35292 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -55,6 +55,14 @@ class Switch : public EntityBase, public EntityBase_DeviceClass { /// The current reported state of the binary sensor. bool state; + /** Control this switch using a boolean state value. + * + * This method provides a unified interface for setting the switch state based on a boolean parameter. + * It automatically calls turn_on() when state is true or turn_off() when state is false. + * + * @param target_state The desired state: true to turn the switch ON, false to turn it OFF. + */ + void control(bool target_state); /** Turn this switch on. This is called by the front-end. * * For implementing switches, please override write_state. diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index b1c81b324a..cae047d168 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -105,8 +105,6 @@ void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) { } void SX126x::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // setup pins this->busy_pin_->setup(); this->rst_pin_->setup(); diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 2d2326549b..8e6db5dc9e 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -50,8 +50,6 @@ void SX127x::write_fifo_(const std::vector &packet) { } void SX127x::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // setup reset this->rst_pin_->setup(); diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index f1b08a505a..67dc924903 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -41,11 +41,13 @@ SX1509KeyTrigger = sx1509_ns.class_( def check_keys(config): - if CONF_KEYS in config: - if len(config[CONF_KEYS]) != config[CONF_KEY_ROWS] * config[CONF_KEY_COLUMNS]: - raise cv.Invalid( - "The number of key codes must equal the number of rows * columns" - ) + if ( + CONF_KEYS in config + and len(config[CONF_KEYS]) != config[CONF_KEY_ROWS] * config[CONF_KEY_COLUMNS] + ): + raise cv.Invalid( + "The number of key codes must equal the number of rows * columns" + ) return config diff --git a/esphome/components/sx1509/output/sx1509_float_output.cpp b/esphome/components/sx1509/output/sx1509_float_output.cpp index e9c401eeed..1d2541bb46 100644 --- a/esphome/components/sx1509/output/sx1509_float_output.cpp +++ b/esphome/components/sx1509/output/sx1509_float_output.cpp @@ -15,7 +15,7 @@ void SX1509FloatOutputChannel::write_state(float state) { } void SX1509FloatOutputChannel::setup() { - ESP_LOGD(TAG, "setup pin %d", this->pin_); + ESP_LOGD(TAG, "Pin %d", this->pin_); this->parent_->pin_mode(this->pin_, gpio::FLAG_OUTPUT); this->parent_->setup_led_driver(this->pin_); this->turn_off(); diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index d323c9a92c..2bf6701dd2 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -8,8 +8,6 @@ namespace sx1509 { static const char *const TAG = "sx1509"; void SX1509Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - ESP_LOGV(TAG, " Resetting devices"); if (!this->write_byte(REG_RESET, 0x12)) { this->mark_failed(); diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index e322a6951d..71468fa932 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -35,7 +35,7 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t severity = LOG_LEVEL_TO_SYSLOG_SEVERITY[level]; } int pri = this->facility_ * 8 + severity; - auto timestamp = this->time_->now().strftime("%b %d %H:%M:%S"); + auto timestamp = this->time_->now().strftime("%b %e %H:%M:%S"); size_t len = message_len; // remove color formatting if (this->strip_ && message[0] == 0x1B && len > 11) { diff --git a/esphome/components/tc74/tc74.cpp b/esphome/components/tc74/tc74.cpp index b79bcb5592..abf3839e00 100644 --- a/esphome/components/tc74/tc74.cpp +++ b/esphome/components/tc74/tc74.cpp @@ -15,7 +15,6 @@ static const uint8_t TC74_DATA_READY_MASK = 0x40; // It is possible the "Data Ready" bit will not be set if the TC74 has not been powered on for at least 250ms, so it not // being set does not constitute a failure. void TC74Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t config_reg; if (this->read_register(TC74_REGISTER_CONFIGURATION, &config_reg, 1) != i2c::ERROR_OK) { this->mark_failed(); diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index cdeb94ceca..edd8af9a27 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -24,7 +24,6 @@ i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffer } void TCA9548AComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t status = 0; if (this->read(&status, 1) != i2c::ERROR_OK) { ESP_LOGE(TAG, "TCA9548A failed"); diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp index 7bd2f44918..b4a04d5b0b 100644 --- a/esphome/components/tca9555/tca9555.cpp +++ b/esphome/components/tca9555/tca9555.cpp @@ -16,7 +16,6 @@ namespace tca9555 { static const char *const TAG = "tca9555"; void TCA9555Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); if (!this->read_gpio_modes_()) { this->mark_failed(); return; diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 9926ebc553..e4e5547595 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -18,7 +18,6 @@ static const uint8_t TCS34725_REGISTER_ENABLE = TCS34725_COMMAND_BIT | 0x00; static const uint8_t TCS34725_REGISTER_CRGBDATAL = TCS34725_COMMAND_BIT | 0x14; void TCS34725Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t id; if (this->read_register(TCS34725_REGISTER_ID, &id, 1) != i2c::ERROR_OK) { this->mark_failed(); diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp index 45241627f9..460f446865 100644 --- a/esphome/components/tee501/tee501.cpp +++ b/esphome/components/tee501/tee501.cpp @@ -8,7 +8,6 @@ namespace tee501 { static const char *const TAG = "tee501"; void TEE501Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t address[] = {0x70, 0x29}; this->write(address, 2, false); uint8_t identification[9]; diff --git a/esphome/components/tem3200/tem3200.cpp b/esphome/components/tem3200/tem3200.cpp index c0655d02b8..b31496142c 100644 --- a/esphome/components/tem3200/tem3200.cpp +++ b/esphome/components/tem3200/tem3200.cpp @@ -16,8 +16,6 @@ enum ErrorCode { }; void TEM3200Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - uint8_t status(NONE); uint16_t raw_temperature(0); uint16_t raw_pressure(0); diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 6f743a77ef..11a148830d 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -80,7 +80,6 @@ void TemplateAlarmControlPanel::dump_config() { } void TemplateAlarmControlPanel::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); switch (this->restore_mode_) { case ALARM_CONTROL_PANEL_ALWAYS_DISARMED: this->current_state_ = ACP_STATE_DISARMED; diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index d32c6ac546..84c687536e 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -16,7 +16,6 @@ TemplateCover::TemplateCover() position_trigger_(new Trigger()), tilt_trigger_(new Trigger()) {} void TemplateCover::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); switch (this->restore_mode_) { case COVER_NO_RESTORE: break; diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 0160fab04b..6ec29c8ef0 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -11,7 +11,6 @@ void TemplateSelect::setup() { return; std::string value; - ESP_LOGD(TAG, "Setting up"); if (!this->restore_value_) { value = this->initial_option_; ESP_LOGD(TAG, "State from initial: %s", value.c_str()); diff --git a/esphome/components/template/text/template_text.cpp b/esphome/components/template/text/template_text.cpp index f8d883e848..f5df7287c5 100644 --- a/esphome/components/template/text/template_text.cpp +++ b/esphome/components/template/text/template_text.cpp @@ -11,8 +11,6 @@ void TemplateText::setup() { if (this->f_.has_value()) return; } - - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); std::string value = this->initial_value_; if (!this->pref_) { ESP_LOGD(TAG, "State from initial: %s", value.c_str()); diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index 8421f5e06f..5fa14a2de7 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -16,7 +16,6 @@ TemplateValve::TemplateValve() position_trigger_(new Trigger()) {} void TemplateValve::setup() { - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); switch (this->restore_mode_) { case VALVE_NO_RESTORE: break; diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 8362e09ac0..aa831d1f06 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -151,7 +151,6 @@ async def new_text( @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_TEXT") cg.add_global(text_ns.using) diff --git a/esphome/components/text/text_traits.h b/esphome/components/text/text_traits.h index 952afa70c7..ceaba2dead 100644 --- a/esphome/components/text/text_traits.h +++ b/esphome/components/text/text_traits.h @@ -3,6 +3,7 @@ #include #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" namespace esphome { namespace text { @@ -23,6 +24,7 @@ class TextTraits { // Set/get the pattern. void set_pattern(std::string pattern) { this->pattern_ = std::move(pattern); } std::string get_pattern() const { return this->pattern_; } + StringRef get_pattern_ref() const { return StringRef(this->pattern_); } // Set/get the frontend mode. void set_mode(TextMode mode) { this->mode_ = mode; } diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index abb2dcae6c..e4aa701a7b 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -162,6 +162,7 @@ def text_sensor_schema( device_class: str = cv.UNDEFINED, entity_category: str = cv.UNDEFINED, icon: str = cv.UNDEFINED, + filters: list = cv.UNDEFINED, ) -> cv.Schema: schema = {} @@ -172,6 +173,7 @@ def text_sensor_schema( (CONF_ICON, icon, cv.icon), (CONF_DEVICE_CLASS, device_class, validate_device_class), (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + (CONF_FILTERS, filters, validate_filters), ]: if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator @@ -230,7 +232,6 @@ async def new_text_sensor(config, *args): @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_TEXT_SENSOR") cg.add_global(text_sensor_ns.using) 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/thermostat/climate.py b/esphome/components/thermostat/climate.py index 0314d877a3..5d0d9442e8 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -477,11 +477,11 @@ def validate_thermostat(config): if ( CONF_ON_BOOT_RESTORE_FROM in config and config[CONF_ON_BOOT_RESTORE_FROM] is OnBootRestoreFrom.DEFAULT_PRESET + and CONF_DEFAULT_PRESET not in config ): - if CONF_DEFAULT_PRESET not in config: - raise cv.Invalid( - f"{CONF_DEFAULT_PRESET} must be defined to use {CONF_ON_BOOT_RESTORE_FROM} in DEFAULT_PRESET mode" - ) + raise cv.Invalid( + f"{CONF_DEFAULT_PRESET} must be defined to use {CONF_ON_BOOT_RESTORE_FROM} in DEFAULT_PRESET mode" + ) if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config: raise cv.Invalid( diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index ab821d457b..a38ad4eae3 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,11 +26,11 @@ 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__) -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True time_ns = cg.esphome_ns.namespace("time") @@ -235,7 +236,7 @@ def validate_time_at(value): def validate_cron_keys(value): if CONF_CRON in value: - for key in value.keys(): + for key in value: if key in CRON_KEYS: raise cv.Invalid(f"Cannot use option {key} when cron: is specified.") if CONF_AT in value: @@ -245,7 +246,7 @@ def validate_cron_keys(value): value.update(cron_) return value if CONF_AT in value: - for key in value.keys(): + for key in value: if key in CRON_KEYS: raise cv.Invalid(f"Cannot use option {key} when at: is specified.") at_ = value[CONF_AT] @@ -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/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp index b1aad42bd7..a524f92f75 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -71,8 +71,6 @@ static const uint8_t LDR_PWM = 0x02; static const uint8_t LDR_GRPPWM = 0x03; void TLC59208FOutput::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - ESP_LOGV(TAG, " Resetting all devices on the bus"); // Reset all devices on the bus diff --git a/esphome/components/tlc5947/tlc5947.cpp b/esphome/components/tlc5947/tlc5947.cpp index 5a5c0c17c0..6d4e099f7a 100644 --- a/esphome/components/tlc5947/tlc5947.cpp +++ b/esphome/components/tlc5947/tlc5947.cpp @@ -19,8 +19,6 @@ void TLC5947::setup() { } this->pwm_amounts_.resize(this->num_chips_ * N_CHANNELS_PER_CHIP, 0); - - ESP_LOGCONFIG(TAG, "Done setting up TLC5947 output component."); } void TLC5947::dump_config() { ESP_LOGCONFIG(TAG, "TLC5947:"); diff --git a/esphome/components/tlc5971/tlc5971.cpp b/esphome/components/tlc5971/tlc5971.cpp index 05ff0a0080..719ab7c2b3 100644 --- a/esphome/components/tlc5971/tlc5971.cpp +++ b/esphome/components/tlc5971/tlc5971.cpp @@ -13,8 +13,6 @@ void TLC5971::setup() { this->clock_pin_->digital_write(true); this->pwm_amounts_.resize(this->num_chips_ * N_CHANNELS_PER_CHIP, 0); - - ESP_LOGCONFIG(TAG, "Done setting up TLC5971 output component."); } void TLC5971::dump_config() { ESP_LOGCONFIG(TAG, "TLC5971:"); diff --git a/esphome/components/tm1621/tm1621.cpp b/esphome/components/tm1621/tm1621.cpp index 502e45b35e..6859973857 100644 --- a/esphome/components/tm1621/tm1621.cpp +++ b/esphome/components/tm1621/tm1621.cpp @@ -29,8 +29,6 @@ const uint8_t TM1621_DIGIT_ROW[2][12] = {{0x5F, 0x50, 0x3D, 0x79, 0x72, 0x6B, 0x {0xF5, 0x05, 0xB6, 0x97, 0x47, 0xD3, 0xF3, 0x85, 0xF7, 0xD7, 0x02, 0x00}}; void TM1621Display::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->cs_pin_->setup(); // OUTPUT this->cs_pin_->digital_write(true); this->data_pin_->setup(); // OUTPUT diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 358a683efb..49da01472f 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -125,8 +125,6 @@ const uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = { 0b01100011, // '~', ord 0x7E (degree symbol) }; void TM1637Display::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->clk_pin_->setup(); // OUTPUT this->clk_pin_->digital_write(false); // LOW this->dio_pin_->setup(); // OUTPUT diff --git a/esphome/components/tm1638/tm1638.cpp b/esphome/components/tm1638/tm1638.cpp index f43b496b35..7ba63fe218 100644 --- a/esphome/components/tm1638/tm1638.cpp +++ b/esphome/components/tm1638/tm1638.cpp @@ -20,8 +20,6 @@ static const uint8_t TM1638_UNKNOWN_CHAR = 0b11111111; static const uint8_t TM1638_SHIFT_DELAY = 4; // clock pause between commands, default 4ms void TM1638Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->clk_pin_->setup(); // OUTPUT this->dio_pin_->setup(); // OUTPUT this->stb_pin_->setup(); // OUTPUT diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index 153cc690e7..49796d9b42 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -10,26 +10,28 @@ from esphome.const import ( CONF_LEVEL, ) -CODEOWNERS = ["@freekode"] +CODEOWNERS = ["@mrtoy-me"] + +CONF_LEVEL_PERCENT = "level_percent" tm1651_ns = cg.esphome_ns.namespace("tm1651") TM1651Brightness = tm1651_ns.enum("TM1651Brightness") TM1651Display = tm1651_ns.class_("TM1651Display", cg.Component) -SetLevelPercentAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) -SetLevelAction = tm1651_ns.class_("SetLevelAction", automation.Action) SetBrightnessAction = tm1651_ns.class_("SetBrightnessAction", automation.Action) -TurnOnAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) -TurnOffAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) - -CONF_LEVEL_PERCENT = "level_percent" +SetLevelAction = tm1651_ns.class_("SetLevelAction", automation.Action) +SetLevelPercentAction = tm1651_ns.class_("SetLevelPercentAction", automation.Action) +TurnOnAction = tm1651_ns.class_("TurnOnAction", automation.Action) +TurnOffAction = tm1651_ns.class_("TurnOffAction", automation.Action) TM1651_BRIGHTNESS_OPTIONS = { - 1: TM1651Brightness.TM1651_BRIGHTNESS_LOW, - 2: TM1651Brightness.TM1651_BRIGHTNESS_MEDIUM, - 3: TM1651Brightness.TM1651_BRIGHTNESS_HIGH, + 1: TM1651Brightness.TM1651_DARKEST, + 2: TM1651Brightness.TM1651_TYPICAL, + 3: TM1651Brightness.TM1651_BRIGHTEST, } +MULTI_CONF = True + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -38,26 +40,21 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_DIO_PIN): pins.internal_gpio_output_pin_schema, } ), - cv.only_with_arduino, ) -validate_level_percent = cv.All(cv.int_range(min=0, max=100)) -validate_level = cv.All(cv.int_range(min=0, max=7)) -validate_brightness = cv.enum(TM1651_BRIGHTNESS_OPTIONS, int=True) - async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - clk_pin = await cg.gpio_pin_expression(config[CONF_CLK_PIN]) cg.add(var.set_clk_pin(clk_pin)) dio_pin = await cg.gpio_pin_expression(config[CONF_DIO_PIN]) cg.add(var.set_dio_pin(dio_pin)) - # https://platformio.org/lib/show/6865/TM1651 - cg.add_library("freekode/TM1651", "1.0.1") +validate_brightness = cv.enum(TM1651_BRIGHTNESS_OPTIONS, int=True) +validate_level = cv.All(cv.int_range(min=0, max=7)) +validate_level_percent = cv.All(cv.int_range(min=0, max=100)) BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id( { @@ -66,38 +63,22 @@ BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("tm1651.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) -async def output_turn_on_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var - - @automation.register_action( - "tm1651.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA -) -async def output_turn_off_to_code(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - return var - - -@automation.register_action( - "tm1651.set_level_percent", - SetLevelPercentAction, + "tm1651.set_brightness", + SetBrightnessAction, cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(TM1651Display), - cv.Required(CONF_LEVEL_PERCENT): cv.templatable(validate_level_percent), + cv.Required(CONF_BRIGHTNESS): cv.templatable(validate_brightness), }, - key=CONF_LEVEL_PERCENT, + key=CONF_BRIGHTNESS, ), ) -async def tm1651_set_level_percent_to_code(config, action_id, template_arg, args): +async def tm1651_set_brightness_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_ = await cg.templatable(config[CONF_LEVEL_PERCENT], args, cg.uint8) - cg.add(var.set_level_percent(template_)) + template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.uint8) + cg.add(var.set_brightness(template_)) return var @@ -121,19 +102,35 @@ async def tm1651_set_level_to_code(config, action_id, template_arg, args): @automation.register_action( - "tm1651.set_brightness", - SetBrightnessAction, + "tm1651.set_level_percent", + SetLevelPercentAction, cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(TM1651Display), - cv.Required(CONF_BRIGHTNESS): cv.templatable(validate_brightness), + cv.Required(CONF_LEVEL_PERCENT): cv.templatable(validate_level_percent), }, - key=CONF_BRIGHTNESS, + key=CONF_LEVEL_PERCENT, ), ) -async def tm1651_set_brightness_to_code(config, action_id, template_arg, args): +async def tm1651_set_level_percent_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_LEVEL_PERCENT], args, cg.uint8) + cg.add(var.set_level_percent(template_)) + return var + + +@automation.register_action( + "tm1651.turn_off", TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA +) +async def output_turn_off_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action("tm1651.turn_on", TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) +async def output_turn_on_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.uint8) - cg.add(var.set_brightness(template_)) return var diff --git a/esphome/components/tm1651/tm1651.cpp b/esphome/components/tm1651/tm1651.cpp index 64c3e62b32..15ada0f8ff 100644 --- a/esphome/components/tm1651/tm1651.cpp +++ b/esphome/components/tm1651/tm1651.cpp @@ -1,7 +1,54 @@ -#ifdef USE_ARDUINO +// This Esphome TM1651 component for use with Mini Battery Displays (7 LED levels) +// and removes the Esphome dependency on the TM1651 Arduino library. +// It was largely based on the work of others as set out below. +// @mrtoy-me July 2025 +// ============================================================================================== +// Original Arduino TM1651 library: +// Author:Fred.Chu +// Date:14 August, 2014 +// Applicable Module: Battery Display v1.0 +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the GNU +// Lesser General Public License for more details. +// Modified record: +// Author: Detlef Giessmann Germany +// Mail: mydiyp@web.de +// Demo for the new 7 LED Battery-Display 2017 +// IDE: Arduino-1.6.5 +// Type: OPEN-SMART CX10*4RY68 4Color +// Date: 01.05.2017 +// ============================================================================================== +// Esphome component using arduino TM1651 library: +// MIT License +// Copyright (c) 2019 freekode +// ============================================================================================== +// Library and command-line (python) program to control mini battery displays on Raspberry Pi: +// MIT License +// Copyright (c) 2020 Koen Vervloese +// ============================================================================================== +// MIT License +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. #include "tm1651.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -9,86 +56,205 @@ namespace tm1651 { static const char *const TAG = "tm1651.display"; -static const uint8_t MAX_INPUT_LEVEL_PERCENT = 100; -static const uint8_t TM1651_MAX_LEVEL = 7; +static const bool LINE_HIGH = true; +static const bool LINE_LOW = false; -static const uint8_t TM1651_BRIGHTNESS_LOW_HW = 0; -static const uint8_t TM1651_BRIGHTNESS_MEDIUM_HW = 2; -static const uint8_t TM1651_BRIGHTNESS_HIGH_HW = 7; +// TM1651 maximum frequency is 500 kHz (duty ratio 50%) = 2 microseconds / cycle +static const uint8_t CLOCK_CYCLE = 8; + +static const uint8_t HALF_CLOCK_CYCLE = CLOCK_CYCLE / 2; +static const uint8_t QUARTER_CLOCK_CYCLE = CLOCK_CYCLE / 4; + +static const uint8_t ADDR_FIXED = 0x44; // fixed address mode +static const uint8_t ADDR_START = 0xC0; // address of the display register + +static const uint8_t DISPLAY_OFF = 0x80; +static const uint8_t DISPLAY_ON = 0x88; + +static const uint8_t MAX_DISPLAY_LEVELS = 7; + +static const uint8_t PERCENT100 = 100; +static const uint8_t PERCENT50 = 50; + +static const uint8_t TM1651_BRIGHTNESS_DARKEST = 0; +static const uint8_t TM1651_BRIGHTNESS_TYPICAL = 2; +static const uint8_t TM1651_BRIGHTNESS_BRIGHTEST = 7; + +static const uint8_t TM1651_LEVEL_TAB[] = {0b00000000, 0b00000001, 0b00000011, 0b00000111, + 0b00001111, 0b00011111, 0b00111111, 0b01111111}; + +// public void TM1651Display::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); + this->clk_pin_->setup(); + this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); - uint8_t clk = clk_pin_->get_pin(); - uint8_t dio = dio_pin_->get_pin(); + this->dio_pin_->setup(); + this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT); - battery_display_ = make_unique(clk, dio); - battery_display_->init(); - battery_display_->clearDisplay(); + this->brightness_ = TM1651_BRIGHTNESS_TYPICAL; + + // clear display + this->display_level_(); + this->update_brightness_(DISPLAY_ON); } void TM1651Display::dump_config() { - ESP_LOGCONFIG(TAG, "TM1651 Battery Display"); + ESP_LOGCONFIG(TAG, "Battery Display"); LOG_PIN(" CLK: ", clk_pin_); LOG_PIN(" DIO: ", dio_pin_); } -void TM1651Display::set_level_percent(uint8_t new_level) { - this->level_ = calculate_level_(new_level); - this->repaint_(); +void TM1651Display::set_brightness(uint8_t new_brightness) { + this->brightness_ = this->remap_brightness_(new_brightness); + if (this->display_on_) { + this->update_brightness_(DISPLAY_ON); + } } void TM1651Display::set_level(uint8_t new_level) { + if (new_level > MAX_DISPLAY_LEVELS) + new_level = MAX_DISPLAY_LEVELS; this->level_ = new_level; - this->repaint_(); + if (this->display_on_) { + this->display_level_(); + } } -void TM1651Display::set_brightness(uint8_t new_brightness) { - this->brightness_ = calculate_brightness_(new_brightness); - this->repaint_(); -} - -void TM1651Display::turn_on() { - this->is_on_ = true; - this->repaint_(); +void TM1651Display::set_level_percent(uint8_t percentage) { + this->level_ = this->calculate_level_(percentage); + if (this->display_on_) { + this->display_level_(); + } } void TM1651Display::turn_off() { - this->is_on_ = false; - battery_display_->displayLevel(0); + this->display_on_ = false; + this->update_brightness_(DISPLAY_OFF); } -void TM1651Display::repaint_() { - if (!this->is_on_) { - return; - } - - battery_display_->set(this->brightness_); - battery_display_->displayLevel(this->level_); +void TM1651Display::turn_on() { + this->display_on_ = true; + // display level as it could have been changed when display turned off + this->display_level_(); + this->update_brightness_(DISPLAY_ON); } -uint8_t TM1651Display::calculate_level_(uint8_t new_level) { - if (new_level == 0) { - return 0; - } +// protected - float calculated_level = TM1651_MAX_LEVEL / (float) (MAX_INPUT_LEVEL_PERCENT / (float) new_level); - return (uint8_t) roundf(calculated_level); +uint8_t TM1651Display::calculate_level_(uint8_t percentage) { + if (percentage > PERCENT100) + percentage = PERCENT100; + // scale 0-100% to 0-7 display levels + // use integer arithmetic with rounding + uint16_t initial_scaling = (percentage * MAX_DISPLAY_LEVELS) + PERCENT50; + return (uint8_t) (initial_scaling / PERCENT100); } -uint8_t TM1651Display::calculate_brightness_(uint8_t new_brightness) { - if (new_brightness <= 1) { - return TM1651_BRIGHTNESS_LOW_HW; - } else if (new_brightness == 2) { - return TM1651_BRIGHTNESS_MEDIUM_HW; - } else if (new_brightness >= 3) { - return TM1651_BRIGHTNESS_HIGH_HW; +void TM1651Display::display_level_() { + this->start_(); + this->write_byte_(ADDR_FIXED); + this->stop_(); + + this->start_(); + this->write_byte_(ADDR_START); + this->write_byte_(TM1651_LEVEL_TAB[this->level_]); + this->stop_(); +} + +uint8_t TM1651Display::remap_brightness_(uint8_t new_brightness) { + if (new_brightness <= 1) + return TM1651_BRIGHTNESS_DARKEST; + if (new_brightness == 2) + return TM1651_BRIGHTNESS_TYPICAL; + + // new_brightness >= 3 + return TM1651_BRIGHTNESS_BRIGHTEST; +} + +void TM1651Display::update_brightness_(uint8_t on_off_control) { + this->start_(); + this->write_byte_(on_off_control | this->brightness_); + this->stop_(); +} + +// low level functions + +bool TM1651Display::write_byte_(uint8_t data) { + // data bit written to DIO when CLK is low + for (uint8_t i = 0; i < 8; i++) { + this->half_cycle_clock_low_((bool) (data & 0x01)); + this->half_cycle_clock_high_(); + data >>= 1; } - return TM1651_BRIGHTNESS_LOW_HW; + // start 9th cycle, setting DIO high and look for ack + this->half_cycle_clock_low_(LINE_HIGH); + return this->half_cycle_clock_high_ack_(); +} + +void TM1651Display::half_cycle_clock_low_(bool data_bit) { + // first half cycle, clock low and write data bit + this->clk_pin_->digital_write(LINE_LOW); + delayMicroseconds(QUARTER_CLOCK_CYCLE); + + this->dio_pin_->digital_write(data_bit); + delayMicroseconds(QUARTER_CLOCK_CYCLE); +} + +void TM1651Display::half_cycle_clock_high_() { + // second half cycle, clock high + this->clk_pin_->digital_write(LINE_HIGH); + delayMicroseconds(HALF_CLOCK_CYCLE); +} + +bool TM1651Display::half_cycle_clock_high_ack_() { + // second half cycle, clock high and check for ack + this->clk_pin_->digital_write(LINE_HIGH); + delayMicroseconds(QUARTER_CLOCK_CYCLE); + + this->dio_pin_->pin_mode(gpio::FLAG_INPUT); + // valid ack on DIO is low + bool ack = (!this->dio_pin_->digital_read()); + + this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT); + + // ack should be set DIO low by now + // if its not, set DIO low before the next cycle + if (!ack) { + this->dio_pin_->digital_write(LINE_LOW); + } + delayMicroseconds(QUARTER_CLOCK_CYCLE); + + // begin next cycle + this->clk_pin_->digital_write(LINE_LOW); + + return ack; +} + +void TM1651Display::start_() { + // start data transmission + this->delineate_transmission_(LINE_HIGH); +} + +void TM1651Display::stop_() { + // stop data transmission + this->delineate_transmission_(LINE_LOW); +} + +void TM1651Display::delineate_transmission_(bool dio_state) { + // delineate data transmission + // DIO changes its value while CLK is high + + this->dio_pin_->digital_write(dio_state); + delayMicroseconds(HALF_CLOCK_CYCLE); + + this->clk_pin_->digital_write(LINE_HIGH); + delayMicroseconds(QUARTER_CLOCK_CYCLE); + + this->dio_pin_->digital_write(!dio_state); + delayMicroseconds(QUARTER_CLOCK_CYCLE); } } // namespace tm1651 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h index fe7b7d9c6f..7079910adf 100644 --- a/esphome/components/tm1651/tm1651.h +++ b/esphome/components/tm1651/tm1651.h @@ -1,22 +1,16 @@ #pragma once -#ifdef USE_ARDUINO - -#include - +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" -#include "esphome/core/automation.h" - -#include namespace esphome { namespace tm1651 { enum TM1651Brightness : uint8_t { - TM1651_BRIGHTNESS_LOW = 1, - TM1651_BRIGHTNESS_MEDIUM = 2, - TM1651_BRIGHTNESS_HIGH = 3, + TM1651_DARKEST = 1, + TM1651_TYPICAL = 2, + TM1651_BRIGHTEST = 3, }; class TM1651Display : public Component { @@ -27,36 +21,49 @@ class TM1651Display : public Component { void setup() override; void dump_config() override; - void set_level_percent(uint8_t new_level); - void set_level(uint8_t new_level); void set_brightness(uint8_t new_brightness); void set_brightness(TM1651Brightness new_brightness) { this->set_brightness(static_cast(new_brightness)); } - void turn_on(); + void set_level(uint8_t new_level); + void set_level_percent(uint8_t percentage); + void turn_off(); + void turn_on(); protected: - std::unique_ptr battery_display_; + uint8_t calculate_level_(uint8_t percentage); + void display_level_(); + + uint8_t remap_brightness_(uint8_t new_brightness); + void update_brightness_(uint8_t on_off_control); + + // low level functions + bool write_byte_(uint8_t data); + + void half_cycle_clock_low_(bool data_bit); + void half_cycle_clock_high_(); + bool half_cycle_clock_high_ack_(); + + void start_(); + void stop_(); + + void delineate_transmission_(bool dio_state); + InternalGPIOPin *clk_pin_; InternalGPIOPin *dio_pin_; - bool is_on_ = true; - uint8_t brightness_; - uint8_t level_; - - void repaint_(); - - uint8_t calculate_level_(uint8_t new_level); - uint8_t calculate_brightness_(uint8_t new_brightness); + bool display_on_{true}; + uint8_t brightness_{}; + uint8_t level_{0}; }; -template class SetLevelPercentAction : public Action, public Parented { +template class SetBrightnessAction : public Action, public Parented { public: - TEMPLATABLE_VALUE(uint8_t, level_percent) + TEMPLATABLE_VALUE(uint8_t, brightness) void play(Ts... x) override { - auto level_percent = this->level_percent_.value(x...); - this->parent_->set_level_percent(level_percent); + auto brightness = this->brightness_.value(x...); + this->parent_->set_brightness(brightness); } }; @@ -70,13 +77,13 @@ template class SetLevelAction : public Action, public Par } }; -template class SetBrightnessAction : public Action, public Parented { +template class SetLevelPercentAction : public Action, public Parented { public: - TEMPLATABLE_VALUE(uint8_t, brightness) + TEMPLATABLE_VALUE(uint8_t, level_percent) void play(Ts... x) override { - auto brightness = this->brightness_.value(x...); - this->parent_->set_brightness(brightness); + auto level_percent = this->level_percent_.value(x...); + this->parent_->set_level_percent(level_percent); } }; @@ -92,5 +99,3 @@ template class TurnOffAction : public Action, public Pare } // namespace tm1651 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/tmp117/tmp117.cpp b/esphome/components/tmp117/tmp117.cpp index 5fe8f51414..c9eff41399 100644 --- a/esphome/components/tmp117/tmp117.cpp +++ b/esphome/components/tmp117/tmp117.cpp @@ -26,8 +26,6 @@ void TMP117Component::update() { } } void TMP117Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - if (!this->write_config_(this->config_)) { this->mark_failed(); return; diff --git a/esphome/components/tsl2561/tsl2561.cpp b/esphome/components/tsl2561/tsl2561.cpp index 1b5c9f2635..1442dd176c 100644 --- a/esphome/components/tsl2561/tsl2561.cpp +++ b/esphome/components/tsl2561/tsl2561.cpp @@ -15,7 +15,6 @@ static const uint8_t TSL2561_REGISTER_DATA_0 = 0x0C; static const uint8_t TSL2561_REGISTER_DATA_1 = 0x0E; void TSL2561Sensor::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t id; if (!this->tsl2561_read_byte(TSL2561_REGISTER_ID, &id)) { this->mark_failed(); diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index c7622b116a..999e42e949 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -43,7 +43,6 @@ void TSL2591Component::disable_if_power_saving_() { } void TSL2591Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup for address 0x%02X", this->address_); switch (this->component_gain_) { case TSL2591_CGAIN_LOW: this->gain_ = TSL2591_GAIN_LOW; diff --git a/esphome/components/tt21100/touchscreen/tt21100.cpp b/esphome/components/tt21100/touchscreen/tt21100.cpp index d4dd1c195f..b4735fe6d7 100644 --- a/esphome/components/tt21100/touchscreen/tt21100.cpp +++ b/esphome/components/tt21100/touchscreen/tt21100.cpp @@ -47,8 +47,6 @@ struct TT21100TouchReport { float TT21100Touchscreen::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } void TT21100Touchscreen::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Register interrupt pin if (this->interrupt_pin_ != nullptr) { this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); diff --git a/esphome/components/ttp229_bsf/ttp229_bsf.cpp b/esphome/components/ttp229_bsf/ttp229_bsf.cpp index 8b58795ebb..8d1ed45bb0 100644 --- a/esphome/components/ttp229_bsf/ttp229_bsf.cpp +++ b/esphome/components/ttp229_bsf/ttp229_bsf.cpp @@ -7,7 +7,6 @@ namespace ttp229_bsf { static const char *const TAG = "ttp229_bsf"; void TTP229BSFComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->sdo_pin_->setup(); this->scl_pin_->setup(); this->scl_pin_->digital_write(true); diff --git a/esphome/components/ttp229_lsf/ttp229_lsf.cpp b/esphome/components/ttp229_lsf/ttp229_lsf.cpp index 8e976da4ef..7bdb57ebec 100644 --- a/esphome/components/ttp229_lsf/ttp229_lsf.cpp +++ b/esphome/components/ttp229_lsf/ttp229_lsf.cpp @@ -7,7 +7,6 @@ namespace ttp229_lsf { static const char *const TAG = "ttp229_lsf"; void TTP229LSFComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t data[2]; if (this->read(data, 2) != i2c::ERROR_OK) { this->error_code_ = COMMUNICATION_FAILED; diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py index 4dbdf07651..cefe5d5a4b 100644 --- a/esphome/components/tuya/climate/__init__.py +++ b/esphome/components/tuya/climate/__init__.py @@ -46,16 +46,15 @@ TuyaClimate = tuya_ns.class_("TuyaClimate", climate.Climate, cg.Component) def validate_temperature_multipliers(value): - if CONF_TEMPERATURE_MULTIPLIER in value: - if ( - CONF_CURRENT_TEMPERATURE_MULTIPLIER in value - or CONF_TARGET_TEMPERATURE_MULTIPLIER in value - ): - raise cv.Invalid( - f"Cannot have {CONF_TEMPERATURE_MULTIPLIER} at the same time as " - f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} and " - f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}" - ) + if CONF_TEMPERATURE_MULTIPLIER in value and ( + CONF_CURRENT_TEMPERATURE_MULTIPLIER in value + or CONF_TARGET_TEMPERATURE_MULTIPLIER in value + ): + raise cv.Invalid( + f"Cannot have {CONF_TEMPERATURE_MULTIPLIER} at the same time as " + f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} and " + f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}" + ) if ( CONF_CURRENT_TEMPERATURE_MULTIPLIER in value and CONF_TARGET_TEMPERATURE_MULTIPLIER not in value diff --git a/esphome/components/tuya/number/__init__.py b/esphome/components/tuya/number/__init__.py index bd57c8be14..7a61e69c5c 100644 --- a/esphome/components/tuya/number/__init__.py +++ b/esphome/components/tuya/number/__init__.py @@ -34,12 +34,14 @@ def validate_min_max(config): min_value = config[CONF_MIN_VALUE] if max_value <= min_value: raise cv.Invalid("max_value must be greater than min_value") - if hidden_config := config.get(CONF_DATAPOINT_HIDDEN): - if (initial_value := hidden_config.get(CONF_INITIAL_VALUE, None)) is not None: - if (initial_value > max_value) or (initial_value < min_value): - raise cv.Invalid( - f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}" - ) + if ( + (hidden_config := config.get(CONF_DATAPOINT_HIDDEN)) + and (initial_value := hidden_config.get(CONF_INITIAL_VALUE, None)) is not None + and ((initial_value > max_value) or (initial_value < min_value)) + ): + raise cv.Invalid( + f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}" + ) return config diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index 42e3955fc2..fd7b5fb03f 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -15,7 +15,6 @@ static const char *const DIRECTIONS[] = {"N", "NNE", "NE", "ENE", "E", "ESE", "S "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"}; void Tx20Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->pin_->setup(); this->store_.buffer = new uint16_t[MAX_BUFFER_SIZE]; diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index 7441d8c1b3..4a1c326789 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -74,7 +74,6 @@ uint32_t ESP32ArduinoUARTComponent::get_config() { } void ESP32ArduinoUARTComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // Use Arduino HardwareSerial UARTs if all used pins match the ones // preconfigured by the platform. For example if RX disabled but TX pin // is 1 we still want to use Serial. diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 7f4cc7b37c..b2bf2bacf1 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -56,7 +56,6 @@ uint32_t ESP8266UartComponent::get_config() { } void ESP8266UartComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); // Use Arduino HardwareSerial UARTs if all used pins match the ones // preconfigured by the platform. For example if RX disabled but TX pin // is 1 we still want to use Serial. diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 63b2579c3f..6bb4b16819 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -86,8 +86,6 @@ void IDFUARTComponent::setup() { return; } this->uart_num_ = static_cast(next_uart_num++); - ESP_LOGCONFIG(TAG, "Running setup for UART %u", this->uart_num_); - this->lock_ = xSemaphoreCreateMutex(); xSemaphoreTake(this->lock_, portMAX_DELAY); diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index ffdb329669..8a7a301cfe 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -46,8 +46,6 @@ uint16_t LibreTinyUARTComponent::get_config() { } void LibreTinyUARTComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin(); int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin(); bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index f375d4a93f..ae3042fb77 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -52,8 +52,6 @@ uint16_t RP2040UartComponent::get_config() { } void RP2040UartComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - uint16_t config = get_config(); constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28}); diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index ed405d7c22..6b1e4f8ed8 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -1,6 +1,7 @@ from esphome import automation from esphome.automation import Trigger import esphome.codegen as cg +from esphome.components.const import CONF_ON_RECEIVE from esphome.components.packet_transport import ( CONF_BINARY_SENSORS, CONF_ENCRYPTION, @@ -27,7 +28,6 @@ trigger_args = cg.std_vector.template(cg.uint8) CONF_ADDRESSES = "addresses" CONF_LISTEN_ADDRESS = "listen_address" CONF_UDP_ID = "udp_id" -CONF_ON_RECEIVE = "on_receive" CONF_LISTEN_PORT = "listen_port" CONF_BROADCAST_PORT = "broadcast_port" diff --git a/esphome/components/ufire_ec/sensor.py b/esphome/components/ufire_ec/sensor.py index 944fdfdee9..9edf0f89ff 100644 --- a/esphome/components/ufire_ec/sensor.py +++ b/esphome/components/ufire_ec/sensor.py @@ -122,5 +122,4 @@ UFIRE_EC_RESET_SCHEMA = cv.Schema( ) async def ufire_ec_reset_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index 813e667a00..364a133776 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -7,8 +7,6 @@ namespace ufire_ec { static const char *const TAG = "ufire_ec"; void UFireECComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - uint8_t version; if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) { this->mark_failed(); diff --git a/esphome/components/ufire_ise/sensor.py b/esphome/components/ufire_ise/sensor.py index e57a1155a4..8009cdaa6a 100644 --- a/esphome/components/ufire_ise/sensor.py +++ b/esphome/components/ufire_ise/sensor.py @@ -123,5 +123,4 @@ UFIRE_ISE_RESET_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(UFireISEComponent ) async def ufire_ise_reset_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - return var + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/ufire_ise/ufire_ise.cpp b/esphome/components/ufire_ise/ufire_ise.cpp index 5d0cb6ec2f..503d993fb7 100644 --- a/esphome/components/ufire_ise/ufire_ise.cpp +++ b/esphome/components/ufire_ise/ufire_ise.cpp @@ -9,8 +9,6 @@ namespace ufire_ise { static const char *const TAG = "ufire_ise"; void UFireISEComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - uint8_t version; if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) { this->mark_failed(); diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index b737dfa4cd..e864ea6419 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -8,7 +8,6 @@ namespace ultrasonic { static const char *const TAG = "ultrasonic.sensor"; void UltrasonicSensorComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->trigger_pin_->setup(); this->trigger_pin_->digital_write(false); this->echo_pin_->setup(); diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 758267f412..50d8aaf139 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -126,7 +126,6 @@ async def new_update(config): @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_UPDATE") cg.add_global(update_ns.using) 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 edf6c94b07..4c0c12fa18 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -173,7 +173,6 @@ void USBClient::setup() { usb_host_transfer_alloc(64, 0, &trq->transfer); trq->client = this; } - ESP_LOGCONFIG(TAG, "client setup complete"); } void USBClient::loop() { diff --git a/esphome/components/usb_host/usb_host_component.cpp b/esphome/components/usb_host/usb_host_component.cpp index 63a2ab77cc..682026a9c5 100644 --- a/esphome/components/usb_host/usb_host_component.cpp +++ b/esphome/components/usb_host/usb_host_component.cpp @@ -8,7 +8,6 @@ namespace esphome { namespace usb_host { void USBHost::setup() { - ESP_LOGCONFIG(TAG, "Setup starts"); usb_host_config_t config{}; if (usb_host_install(&config) != ESP_OK) { diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index cb27546120..53254068af 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -235,5 +235,4 @@ async def valve_control_to_code(config, action_id, template_arg, args): @coroutine_with_priority(100.0) async def to_code(config): - cg.add_define("USE_VALVE") cg.add_global(valve_ns.using) diff --git a/esphome/components/veml3235/veml3235.cpp b/esphome/components/veml3235/veml3235.cpp index d5489216b6..f3016fb171 100644 --- a/esphome/components/veml3235/veml3235.cpp +++ b/esphome/components/veml3235/veml3235.cpp @@ -9,9 +9,6 @@ static const char *const TAG = "veml3235.sensor"; void VEML3235Sensor::setup() { uint8_t device_id[] = {0, 0}; - - ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str()); - if (!this->refresh_config_reg()) { ESP_LOGE(TAG, "Unable to write configuration"); this->mark_failed(); diff --git a/esphome/components/veml7700/veml7700.cpp b/esphome/components/veml7700/veml7700.cpp index 9d87a639a6..2a4c246ac9 100644 --- a/esphome/components/veml7700/veml7700.cpp +++ b/esphome/components/veml7700/veml7700.cpp @@ -78,8 +78,6 @@ static const char *get_gain_str(Gain gain) { } void VEML7700Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - auto err = this->configure_(); if (err != i2c::ERROR_OK) { ESP_LOGW(TAG, "Sensor configuration failed"); 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/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index d0b7116eb8..d2548a5bbd 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -32,8 +32,6 @@ void VL53L0XSensor::dump_config() { } void VL53L0XSensor::setup() { - ESP_LOGD(TAG, "'%s' - setup BEGIN", this->name_.c_str()); - if (!esphome::vl53l0x::VL53L0XSensor::enable_pin_setup_complete) { for (auto &vl53_sensor : vl53_sensors) { if (vl53_sensor->enable_pin_ != nullptr) { @@ -258,8 +256,6 @@ void VL53L0XSensor::setup() { // I2C_SXXXX__DEVICE_ADDRESS = 0x0001 for VL53L1X reg(0x8A) = final_address & 0x7F; this->set_i2c_address(final_address); - - ESP_LOGD(TAG, "'%s' - setup END", this->name_.c_str()); } void VL53L0XSensor::update() { diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 647bbc7653..743c90e700 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -238,10 +238,10 @@ void VoiceAssistant::loop() { api::VoiceAssistantRequest msg; msg.start = true; - msg.conversation_id = this->conversation_id_; + msg.set_conversation_id(StringRef(this->conversation_id_)); msg.flags = flags; msg.audio_settings = audio_settings; - msg.wake_word_phrase = this->wake_word_; + msg.set_wake_word_phrase(StringRef(this->wake_word_)); this->wake_word_ = ""; // Reset media player state tracking @@ -251,7 +251,8 @@ void VoiceAssistant::loop() { } #endif - if (this->api_client_ == nullptr || !this->api_client_->send_message(msg)) { + 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; @@ -272,8 +273,8 @@ void VoiceAssistant::loop() { size_t read_bytes = this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0); 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); + msg.set_data(this->send_buffer_, read_bytes); + this->api_client_->send_message(msg, api::VoiceAssistantAudio::MESSAGE_TYPE); } else { if (!this->udp_socket_running_) { if (!this->start_udp_socket_()) { @@ -352,7 +353,7 @@ void VoiceAssistant::loop() { api::VoiceAssistantAnnounceFinished msg; msg.success = true; - this->api_client_->send_message(msg); + this->api_client_->send_message(msg, api::VoiceAssistantAnnounceFinished::MESSAGE_TYPE); break; } } @@ -605,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_() { @@ -615,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); }); } diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 572b75a8f1..695757e137 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -298,6 +298,7 @@ async def to_code(config): if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS") if CONF_AUTH in config: + cg.add_define("USE_WEBSERVER_AUTH") cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME])) cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD])) if CONF_CSS_INCLUDE in config: @@ -317,3 +318,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/web_server.cpp b/esphome/components/web_server/web_server.cpp index bb2640b539..92c5961f87 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -279,7 +279,6 @@ std::string WebServer::get_config_json() { } void WebServer::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->setup_controller(this->include_internal_); this->base_->init(); @@ -377,23 +376,32 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { } #endif -#define set_json_id(root, obj, sensor, start_config) \ - (root)["id"] = sensor; \ - if (((start_config) == DETAIL_ALL)) { \ - (root)["name"] = (obj)->get_name(); \ - (root)["icon"] = (obj)->get_icon(); \ - (root)["entity_category"] = (obj)->get_entity_category(); \ - if ((obj)->is_disabled_by_default()) \ - (root)["is_disabled_by_default"] = (obj)->is_disabled_by_default(); \ +// Helper functions to reduce code size by avoiding macro expansion +static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id, JsonDetail start_config) { + root["id"] = id; + if (start_config == DETAIL_ALL) { + root["name"] = obj->get_name(); + root["icon"] = obj->get_icon(); + root["entity_category"] = obj->get_entity_category(); + bool is_disabled = obj->is_disabled_by_default(); + if (is_disabled) + root["is_disabled_by_default"] = is_disabled; } +} -#define set_json_value(root, obj, sensor, value, start_config) \ - set_json_id((root), (obj), sensor, start_config); \ - (root)["value"] = value; +template +static void set_json_value(JsonObject &root, EntityBase *obj, const std::string &id, const T &value, + JsonDetail start_config) { + set_json_id(root, obj, id, start_config); + root["value"] = value; +} -#define set_json_icon_state_value(root, obj, sensor, state, value, start_config) \ - set_json_value(root, obj, sensor, value, start_config); \ - (root)["state"] = state; +template +static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const std::string &id, + const std::string &state, const T &value, JsonDetail start_config) { + set_json_value(root, obj, id, value, start_config); + root["state"] = state; +} // Helper to get request detail parameter static JsonDetail get_request_detail(AsyncWebServerRequest *request) { @@ -627,15 +635,8 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } else if (match.method_equals("turn_on") || match.method_equals("turn_off")) { auto call = match.method_equals("turn_on") ? obj->turn_on() : obj->turn_off(); - if (request->hasParam("speed_level")) { - auto speed_level = request->getParam("speed_level")->value(); - auto val = parse_number(speed_level.c_str()); - if (!val.has_value()) { - ESP_LOGW(TAG, "Can't convert '%s' to number!", speed_level.c_str()); - return; - } - call.set_speed(*val); - } + parse_int_param_(request, "speed_level", call, &decltype(call)::set_speed); + if (request->hasParam("oscillation")) { auto speed = request->getParam("oscillation")->value(); auto val = parse_on_off(speed.c_str()); @@ -707,69 +708,26 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa request->send(200); } else if (match.method_equals("turn_on")) { auto call = obj->turn_on(); - if (request->hasParam("brightness")) { - auto brightness = parse_number(request->getParam("brightness")->value().c_str()); - if (brightness.has_value()) { - call.set_brightness(*brightness / 255.0f); - } - } - if (request->hasParam("r")) { - auto r = parse_number(request->getParam("r")->value().c_str()); - if (r.has_value()) { - call.set_red(*r / 255.0f); - } - } - if (request->hasParam("g")) { - auto g = parse_number(request->getParam("g")->value().c_str()); - if (g.has_value()) { - call.set_green(*g / 255.0f); - } - } - if (request->hasParam("b")) { - auto b = parse_number(request->getParam("b")->value().c_str()); - if (b.has_value()) { - call.set_blue(*b / 255.0f); - } - } - if (request->hasParam("white_value")) { - auto white_value = parse_number(request->getParam("white_value")->value().c_str()); - if (white_value.has_value()) { - call.set_white(*white_value / 255.0f); - } - } - if (request->hasParam("color_temp")) { - auto color_temp = parse_number(request->getParam("color_temp")->value().c_str()); - if (color_temp.has_value()) { - call.set_color_temperature(*color_temp); - } - } - if (request->hasParam("flash")) { - auto flash = parse_number(request->getParam("flash")->value().c_str()); - if (flash.has_value()) { - call.set_flash_length(*flash * 1000); - } - } - if (request->hasParam("transition")) { - auto transition = parse_number(request->getParam("transition")->value().c_str()); - if (transition.has_value()) { - call.set_transition_length(*transition * 1000); - } - } - if (request->hasParam("effect")) { - const char *effect = request->getParam("effect")->value().c_str(); - call.set_effect(effect); - } + + // Parse color parameters + parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f); + parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f); + parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f); + parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f); + parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f); + parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature); + + // Parse timing parameters + parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000); + parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); + + parse_string_param_(request, "effect", call, &decltype(call)::set_effect); this->defer([call]() mutable { call.perform(); }); request->send(200); } else if (match.method_equals("turn_off")) { auto call = obj->turn_off(); - if (request->hasParam("transition")) { - auto transition = parse_number(request->getParam("transition")->value().c_str()); - if (transition.has_value()) { - call.set_transition_length(*transition * 1000); - } - } + parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); this->defer([call]() mutable { call.perform(); }); request->send(200); } else { @@ -842,18 +800,8 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa return; } - if (request->hasParam("position")) { - auto position = parse_number(request->getParam("position")->value().c_str()); - if (position.has_value()) { - call.set_position(*position); - } - } - if (request->hasParam("tilt")) { - auto tilt = parse_number(request->getParam("tilt")->value().c_str()); - if (tilt.has_value()) { - call.set_tilt(*tilt); - } - } + parse_float_param_(request, "position", call, &decltype(call)::set_position); + parse_float_param_(request, "tilt", call, &decltype(call)::set_tilt); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -907,11 +855,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - if (request->hasParam("value")) { - auto value = parse_number(request->getParam("value")->value().c_str()); - if (value.has_value()) - call.set_value(*value); - } + parse_float_param_(request, "value", call, &decltype(call)::set_value); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -983,10 +927,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat return; } - if (request->hasParam("value")) { - std::string value = request->getParam("value")->value().c_str(); // NOLINT - call.set_date(value); - } + parse_string_param_(request, "value", call, &decltype(call)::set_date); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1042,10 +983,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat return; } - if (request->hasParam("value")) { - std::string value = request->getParam("value")->value().c_str(); // NOLINT - call.set_time(value); - } + parse_string_param_(request, "value", call, &decltype(call)::set_time); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1100,10 +1038,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur return; } - if (request->hasParam("value")) { - std::string value = request->getParam("value")->value().c_str(); // NOLINT - call.set_datetime(value); - } + parse_string_param_(request, "value", call, &decltype(call)::set_datetime); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1154,10 +1089,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat } auto call = obj->make_call(); - if (request->hasParam("value")) { - String value = request->getParam("value")->value(); - call.set_value(value.c_str()); // NOLINT - } + parse_string_param_(request, "value", call, &decltype(call)::set_value); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1216,11 +1148,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - - if (request->hasParam("option")) { - auto option = request->getParam("option")->value(); - call.set_option(option.c_str()); // NOLINT - } + parse_string_param_(request, "option", call, &decltype(call)::set_option); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1276,38 +1204,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url auto call = obj->make_call(); - if (request->hasParam("mode")) { - auto mode = request->getParam("mode")->value(); - call.set_mode(mode.c_str()); // NOLINT - } + // Parse string mode parameters + parse_string_param_(request, "mode", call, &decltype(call)::set_mode); + parse_string_param_(request, "fan_mode", call, &decltype(call)::set_fan_mode); + parse_string_param_(request, "swing_mode", call, &decltype(call)::set_swing_mode); - if (request->hasParam("fan_mode")) { - auto mode = request->getParam("fan_mode")->value(); - call.set_fan_mode(mode.c_str()); // NOLINT - } - - if (request->hasParam("swing_mode")) { - auto mode = request->getParam("swing_mode")->value(); - call.set_swing_mode(mode.c_str()); // NOLINT - } - - if (request->hasParam("target_temperature_high")) { - auto target_temperature_high = parse_number(request->getParam("target_temperature_high")->value().c_str()); - if (target_temperature_high.has_value()) - call.set_target_temperature_high(*target_temperature_high); - } - - if (request->hasParam("target_temperature_low")) { - auto target_temperature_low = parse_number(request->getParam("target_temperature_low")->value().c_str()); - if (target_temperature_low.has_value()) - call.set_target_temperature_low(*target_temperature_low); - } - - if (request->hasParam("target_temperature")) { - auto target_temperature = parse_number(request->getParam("target_temperature")->value().c_str()); - if (target_temperature.has_value()) - call.set_target_temperature(*target_temperature); - } + // Parse temperature parameters + parse_float_param_(request, "target_temperature_high", call, &decltype(call)::set_target_temperature_high); + parse_float_param_(request, "target_temperature_low", call, &decltype(call)::set_target_temperature_low); + parse_float_param_(request, "target_temperature", call, &decltype(call)::set_target_temperature); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1498,12 +1403,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa return; } - if (request->hasParam("position")) { - auto position = parse_number(request->getParam("position")->value().c_str()); - if (position.has_value()) { - call.set_position(*position); - } - } + parse_float_param_(request, "position", call, &decltype(call)::set_position); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1551,9 +1451,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques } auto call = obj->make_call(); - if (request->hasParam("code")) { - call.set_code(request->getParam("code")->value().c_str()); // NOLINT - } + parse_string_param_(request, "code", call, &decltype(call)::set_code); if (match.method_equals("disarm")) { call.disarm(); @@ -1651,6 +1549,19 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty #endif #ifdef USE_UPDATE +static const char *update_state_to_string(update::UpdateState state) { + switch (state) { + case update::UPDATE_STATE_NO_UPDATE: + return "NO UPDATE"; + case update::UPDATE_STATE_AVAILABLE: + return "UPDATE AVAILABLE"; + case update::UPDATE_STATE_INSTALLING: + return "INSTALLING"; + default: + return "UNKNOWN"; + } +} + void WebServer::on_update(update::UpdateEntity *obj) { if (this->events_.empty()) return; @@ -1690,20 +1601,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c return json::build_json([this, obj, start_config](JsonObject root) { set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); root["value"] = obj->update_info.latest_version; - switch (obj->state) { - case update::UPDATE_STATE_NO_UPDATE: - root["state"] = "NO UPDATE"; - break; - case update::UPDATE_STATE_AVAILABLE: - root["state"] = "UPDATE AVAILABLE"; - break; - case update::UPDATE_STATE_INSTALLING: - root["state"] = "INSTALLING"; - break; - default: - root["state"] = "UNKNOWN"; - break; - } + root["state"] = update_state_to_string(obj->state); if (start_config == DETAIL_ALL) { root["current_version"] = obj->update_info.current_version; root["title"] = obj->update_info.title; @@ -1717,162 +1615,161 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c #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; } @@ -1885,147 +1782,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..6bece732fc 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 @@ -498,6 +498,66 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { protected: void add_sorting_info_(JsonObject &root, EntityBase *entity); + +#ifdef USE_LIGHT + // Helper to parse and apply a float parameter with optional scaling + template + void parse_light_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float), + float scale = 1.0f) { + if (request->hasParam(param_name)) { + auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (value.has_value()) { + (call.*setter)(*value / scale); + } + } + } + + // Helper to parse and apply a uint32_t parameter with optional scaling + template + void parse_light_param_uint_(AsyncWebServerRequest *request, const char *param_name, T &call, + Ret (T::*setter)(uint32_t), uint32_t scale = 1) { + if (request->hasParam(param_name)) { + auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (value.has_value()) { + (call.*setter)(*value * scale); + } + } + } +#endif + + // Generic helper to parse and apply a float parameter + template + void parse_float_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float)) { + if (request->hasParam(param_name)) { + auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (value.has_value()) { + (call.*setter)(*value); + } + } + } + + // Generic helper to parse and apply an int parameter + template + void parse_int_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(int)) { + if (request->hasParam(param_name)) { + auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (value.has_value()) { + (call.*setter)(*value); + } + } + } + + // Generic helper to parse and apply a string parameter + template + void parse_string_param_(AsyncWebServerRequest *request, const char *param_name, T &call, + Ret (T::*setter)(const std::string &)) { + if (request->hasParam(param_name)) { + // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string + std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr) + (call.*setter)(value); + } + } + web_server_base::WebServerBase *base_; #ifdef USE_ARDUINO DeferredUpdateEventSourceList events_; diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 9f3371c233..50ae6b92fa 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import CORE, coroutine_with_priority -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index e1c2bc0b25..6e7097338c 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -14,9 +14,11 @@ WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-av void WebServerBase::add_handler(AsyncWebHandler *handler) { // remove all handlers +#ifdef USE_WEBSERVER_AUTH if (!credentials_.username.empty()) { handler = new internal::AuthMiddlewareHandler(handler, &credentials_); } +#endif this->handlers_.push_back(handler); if (this->server_ != nullptr) { this->server_->addHandler(handler); diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index a475238a37..cfca776ee1 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -41,6 +41,7 @@ class MiddlewareHandler : public AsyncWebHandler { AsyncWebHandler *next_; }; +#ifdef USE_WEBSERVER_AUTH struct Credentials { std::string username; std::string password; @@ -79,6 +80,7 @@ class AuthMiddlewareHandler : public MiddlewareHandler { protected: Credentials *credentials_; }; +#endif } // namespace internal @@ -108,8 +110,10 @@ class WebServerBase : public Component { std::shared_ptr get_server() const { return server_; } float get_setup_priority() const override; +#ifdef USE_WEBSERVER_AUTH void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); } void set_auth_password(std::string auth_password) { credentials_.password = std::move(auth_password); } +#endif void add_handler(AsyncWebHandler *handler); @@ -121,7 +125,9 @@ class WebServerBase : public Component { uint16_t port_{80}; std::shared_ptr server_{nullptr}; std::vector handlers_; +#ifdef USE_WEBSERVER_AUTH internal::Credentials credentials_; +#endif }; } // namespace web_server_base diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 734259093e..3226444b1b 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -116,7 +116,7 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { } // Handle regular form data - if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) { + if (r->content_len > CONFIG_HTTPD_MAX_REQ_HDR_LEN) { ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len); httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); return ESP_FAIL; @@ -223,6 +223,7 @@ void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code this->rsp_ = rsp; } +#ifdef USE_WEBSERVER_AUTH bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const { if (username == nullptr || password == nullptr || *username == 0) { return true; @@ -261,6 +262,7 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const { httpd_resp_set_hdr(*this, "WWW-Authenticate", auth_val.c_str()); httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr); } +#endif AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { auto find = this->params_.find(name); @@ -423,14 +425,14 @@ void AsyncEventSourceResponse::destroy(void *ptr) { void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { DeferredEvent item(source, message_generator); - auto iter = std::find_if(this->deferred_queue_.begin(), this->deferred_queue_.end(), - [&item](const DeferredEvent &test) -> bool { return test == item; }); - - if (iter != this->deferred_queue_.end()) { - (*iter) = item; - } else { - this->deferred_queue_.push_back(item); + // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size + for (auto &event : this->deferred_queue_) { + if (event == item) { + event = item; + return; + } } + this->deferred_queue_.push_back(item); } void AsyncEventSourceResponse::process_deferred_queue_() { diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index e8e40ef9b0..76540ef232 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -115,9 +115,11 @@ class AsyncWebServerRequest { // NOLINTNEXTLINE(readability-identifier-naming) size_t contentLength() const { return this->req_->content_len; } +#ifdef USE_WEBSERVER_AUTH bool authenticate(const char *username, const char *password) const; // NOLINTNEXTLINE(readability-identifier-naming) void requestAuthentication(const char *realm = nullptr) const; +#endif void redirect(const std::string &url); diff --git a/esphome/components/weikai_i2c/weikai_i2c.cpp b/esphome/components/weikai_i2c/weikai_i2c.cpp index 32e7ec4f23..03ac74e070 100644 --- a/esphome/components/weikai_i2c/weikai_i2c.cpp +++ b/esphome/components/weikai_i2c/weikai_i2c.cpp @@ -142,8 +142,7 @@ void WeikaiRegisterI2C::write_fifo(uint8_t *data, size_t length) { void WeikaiComponentI2C::setup() { // before any manipulation we store the address to base_address_ for future use this->base_address_ = this->address_; - ESP_LOGCONFIG(TAG, "Running setup for '%s' with %d UARTs at @%02X", this->get_name(), this->children_.size(), - this->base_address_); + ESP_LOGCONFIG(TAG, "Setup %s (%d UARTs) @ 0x%02X", this->get_name(), this->children_.size(), this->base_address_); // enable all channels this->reg(WKREG_GENA, 0) = GENA_C1EN | GENA_C2EN | GENA_C3EN | GENA_C4EN; diff --git a/esphome/components/weikai_spi/weikai_spi.cpp b/esphome/components/weikai_spi/weikai_spi.cpp index a43e0e6599..7bcb817f09 100644 --- a/esphome/components/weikai_spi/weikai_spi.cpp +++ b/esphome/components/weikai_spi/weikai_spi.cpp @@ -156,7 +156,7 @@ void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) { /////////////////////////////////////////////////////////////////////////////// void WeikaiComponentSPI::setup() { using namespace weikai; - ESP_LOGCONFIG(TAG, "Running setup for '%s' with %d UARTs", this->get_name(), this->children_.size()); + ESP_LOGCONFIG(TAG, "Setup %s (%d UARTs)", this->get_name(), this->children_.size()); this->spi_setup(); // enable all channels this->reg(WKREG_GENA, 0) = GENA_C1EN | GENA_C2EN | GENA_C3EN | GENA_C4EN; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 61f37556ba..ac002eac53 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,10 +1,12 @@ from esphome import automation from esphome.automation import Condition import esphome.codegen as cg +from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.network import IPAddress from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv +from esphome.config_validation import only_with_esp_idf from esphome.const import ( CONF_AP, CONF_BSSID, @@ -192,45 +194,7 @@ def final_validate(config): ) -def final_validate_power_esp32_ble(value): - if not CORE.is_esp32: - return - if value != "NONE": - # WiFi should be in modem sleep (!=NONE) with BLE coexistence - # https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-guides/wifi.html#station-sleep - return - for conflicting in [ - "esp32_ble", - "esp32_ble_beacon", - "esp32_ble_server", - "esp32_ble_tracker", - ]: - if conflicting not in fv.full_config.get(): - continue - - try: - # Only arduino 1.0.5+ and esp-idf impacted - cv.require_framework_version( - esp32_arduino=cv.Version(1, 0, 5), - esp_idf=cv.Version(4, 0, 0), - )(None) - except cv.Invalid: - pass - else: - raise cv.Invalid( - f"power_save_mode NONE is incompatible with {conflicting}. " - f"Please remove the power save mode. See also " - f"https://github.com/esphome/issues/issues/2141#issuecomment-865688582" - ) - - FINAL_VALIDATE_SCHEMA = cv.All( - cv.Schema( - { - cv.Optional(CONF_POWER_SAVE_MODE): final_validate_power_esp32_ble, - }, - extra=cv.ALLOW_EXTRA, - ), final_validate, validate_variant, ) @@ -263,8 +227,6 @@ def _validate(config): networks = config.get(CONF_NETWORKS, []) if not networks: raise cv.Invalid("At least one network required for fast_connect!") - if len(networks) != 1: - raise cv.Invalid("Fast connect can only be used with one network!") if CONF_USE_ADDRESS not in config: use_address = CORE.name + config[CONF_DOMAIN] @@ -334,6 +296,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation( single=True ), + cv.Optional(CONF_USE_PSRAM): cv.All( + only_with_esp_idf, cv.requires_component("psram"), cv.boolean + ), } ), _validate, @@ -442,9 +407,7 @@ async def to_code(config): if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) - elif CORE.is_esp32 and CORE.using_arduino: - cg.add_library("WiFi", None) - elif CORE.is_rp2040: + elif (CORE.is_esp32 and CORE.using_arduino) or CORE.is_rp2040: cg.add_library("WiFi", None) if CORE.is_esp32 and CORE.using_esp_idf: @@ -456,6 +419,8 @@ async def to_code(config): if config[CONF_ENABLE_RRM]: cg.add(var.set_rrm(config[CONF_ENABLE_RRM])) + if config.get(CONF_USE_PSRAM): + add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) cg.add_define("USE_WIFI") # must register before OTA safe mode check diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d717b68340..987e276e0c 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -45,7 +45,6 @@ static const char *const TAG = "wifi"; float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); this->wifi_pre_setup_(); if (this->enable_on_boot_) { this->start(); @@ -92,8 +91,11 @@ void WiFiComponent::start() { } if (this->fast_connect_) { - this->selected_ap_ = this->sta_[0]; - this->load_fast_connect_settings_(); + this->trying_loaded_ap_ = this->load_fast_connect_settings_(); + if (!this->trying_loaded_ap_) { + this->ap_index_ = 0; + this->selected_ap_ = this->sta_[this->ap_index_]; + } this->start_connecting(this->selected_ap_, false); } else { this->start_scanning(); @@ -122,6 +124,14 @@ void WiFiComponent::start() { this->wifi_apply_hostname_(); } +void WiFiComponent::restart_adapter() { + ESP_LOGW(TAG, "Restarting adapter"); + this->wifi_mode_(false, {}); + delay(100); // NOLINT + this->num_retried_ = 0; + this->retry_hidden_ = false; +} + void WiFiComponent::loop() { this->wifi_loop_(); const uint32_t now = App.get_loop_component_start_time(); @@ -141,7 +151,7 @@ void WiFiComponent::loop() { this->status_set_warning("waiting to reconnect"); if (millis() - this->action_started_ > 5000) { if (this->fast_connect_ || this->retry_hidden_) { - this->start_connecting(this->sta_[0], false); + this->start_connecting(this->selected_ap_, false); } else { this->start_scanning(); } @@ -261,10 +271,8 @@ void WiFiComponent::setup_ap_config_() { } this->ap_.set_ssid(name); } - - ESP_LOGCONFIG(TAG, "Setting up AP"); - ESP_LOGCONFIG(TAG, + "Setting up AP:\n" " AP SSID: '%s'\n" " AP Password: '%s'", this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str()); @@ -449,9 +457,11 @@ void WiFiComponent::print_connect_params_() { " Signal strength: %d dB %s", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi))); +#ifdef ESPHOME_LOG_HAS_VERBOSE if (this->selected_ap_.get_bssid().has_value()) { ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid())); } +#endif ESP_LOGCONFIG(TAG, " Channel: %" PRId32 "\n" " Subnet: %s\n" @@ -497,6 +507,54 @@ void WiFiComponent::start_scanning() { this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING; } +// Helper function for WiFi scan result comparison +// Returns true if 'a' should be placed before 'b' in the sorted order +[[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) { + // Matching networks always come before non-matching + if (a.get_matches() && !b.get_matches()) + return true; + if (!a.get_matches() && b.get_matches()) + return false; + + if (a.get_matches() && b.get_matches()) { + // For APs with the same SSID, always prefer stronger signal + // This helps with mesh networks and multiple APs + if (a.get_ssid() == b.get_ssid()) { + return a.get_rssi() > b.get_rssi(); + } + + // For different SSIDs, check priority first + if (a.get_priority() != b.get_priority()) + return a.get_priority() > b.get_priority(); + // If priorities are equal, prefer stronger signal + return a.get_rssi() > b.get_rssi(); + } + + // Both don't match - sort by signal strength + return a.get_rssi() > b.get_rssi(); +} + +// Helper function for insertion sort of WiFi scan results +// Using insertion sort instead of std::stable_sort saves flash memory +// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) +// IMPORTANT: This sort is stable (preserves relative order of equal elements) +static void insertion_sort_scan_results(std::vector &results) { + const size_t size = results.size(); + for (size_t i = 1; i < size; i++) { + // Make a copy to avoid issues with move semantics during comparison + WiFiScanResult key = results[i]; + int32_t j = i - 1; + + // Move elements that are worse than key to the right + // For stability, we only move if key is strictly better than results[j] + while (j >= 0 && wifi_scan_result_is_better(key, results[j])) { + results[j + 1] = results[j]; + j--; + } + results[j + 1] = key; + } +} + void WiFiComponent::check_scanning_finished() { if (!this->scan_done_) { if (millis() - this->action_started_ > 30000) { @@ -527,22 +585,8 @@ void WiFiComponent::check_scanning_finished() { } } - std::stable_sort(this->scan_result_.begin(), this->scan_result_.end(), - [](const WiFiScanResult &a, const WiFiScanResult &b) { - // return true if a is better than b - if (a.get_matches() && !b.get_matches()) - return true; - if (!a.get_matches() && b.get_matches()) - return false; - - if (a.get_matches() && b.get_matches()) { - // if both match, check priority - if (a.get_priority() != b.get_priority()) - return a.get_priority() > b.get_priority(); - } - - return a.get_rssi() > b.get_rssi(); - }); + // Sort scan results using insertion sort for better memory efficiency + insertion_sort_scan_results(this->scan_result_); for (auto &res : this->scan_result_) { char bssid_s[18]; @@ -552,8 +596,10 @@ void WiFiComponent::check_scanning_finished() { if (res.get_matches()) { ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? "(HIDDEN) " : "", bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - ESP_LOGD(TAG, " Channel: %u", res.get_channel()); - ESP_LOGD(TAG, " RSSI: %d dB", res.get_rssi()); + ESP_LOGD(TAG, + " Channel: %u\n" + " RSSI: %d dB", + res.get_channel(), res.get_rssi()); } else { ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); @@ -698,18 +744,30 @@ void WiFiComponent::retry_connect() { delay(10); if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && (this->num_retried_ > 3 || this->error_from_callback_)) { - if (this->num_retried_ > 5) { - // If retry failed for more than 5 times, let's restart STA - ESP_LOGW(TAG, "Restarting adapter"); - this->wifi_mode_(false, {}); - delay(100); // NOLINT + if (this->fast_connect_) { + if (this->trying_loaded_ap_) { + this->trying_loaded_ap_ = false; + this->ap_index_ = 0; // Retry from the first configured AP + } else if (this->ap_index_ >= this->sta_.size() - 1) { + ESP_LOGW(TAG, "No more APs to try"); + this->ap_index_ = 0; + this->restart_adapter(); + } else { + // Try next AP + this->ap_index_++; + } this->num_retried_ = 0; - this->retry_hidden_ = false; + this->selected_ap_ = this->sta_[this->ap_index_]; } else { - // Try hidden networks after 3 failed retries - ESP_LOGD(TAG, "Retrying with hidden networks"); - this->retry_hidden_ = true; - this->num_retried_++; + if (this->num_retried_ > 5) { + // If retry failed for more than 5 times, let's restart STA + this->restart_adapter(); + } else { + // Try hidden networks after 3 failed retries + ESP_LOGD(TAG, "Retrying with hidden networks"); + this->retry_hidden_ = true; + this->num_retried_++; + } } } else { this->num_retried_++; @@ -756,17 +814,22 @@ bool WiFiComponent::is_esp32_improv_active_() { #endif } -void WiFiComponent::load_fast_connect_settings_() { +bool WiFiComponent::load_fast_connect_settings_() { SavedWifiFastConnectSettings fast_connect_save{}; if (this->fast_connect_pref_.load(&fast_connect_save)) { bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); + this->ap_index_ = fast_connect_save.ap_index; + this->selected_ap_ = this->sta_[this->ap_index_]; this->selected_ap_.set_bssid(bssid); this->selected_ap_.set_channel(fast_connect_save.channel); ESP_LOGD(TAG, "Loaded fast_connect settings"); + return true; } + + return false; } void WiFiComponent::save_fast_connect_settings_() { @@ -778,6 +841,7 @@ void WiFiComponent::save_fast_connect_settings_() { memcpy(fast_connect_save.bssid, bssid.data(), 6); fast_connect_save.channel = channel; + fast_connect_save.ap_index = this->ap_index_; this->fast_connect_pref_.save(&fast_connect_save); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 64797a5801..bbe1bbb874 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -60,6 +60,7 @@ struct SavedWifiSettings { struct SavedWifiFastConnectSettings { uint8_t bssid[6]; uint8_t channel; + int8_t ap_index; } PACKED; // NOLINT enum WiFiComponentState : uint8_t { @@ -256,6 +257,7 @@ class WiFiComponent : public Component { void setup() override; void start(); void dump_config() override; + void restart_adapter(); /// WIFI setup_priority. float get_setup_priority() const override; float get_loop_priority() const override; @@ -353,7 +355,7 @@ class WiFiComponent : public Component { bool is_captive_portal_active_(); bool is_esp32_improv_active_(); - void load_fast_connect_settings_(); + bool load_fast_connect_settings_(); void save_fast_connect_settings_(); #ifdef USE_ESP8266 @@ -400,12 +402,14 @@ class WiFiComponent : public Component { WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; uint8_t num_retried_{0}; + uint8_t ap_index_{0}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ // Group all boolean values together bool fast_connect_{false}; + bool trying_loaded_ap_{false}; bool retry_hidden_{false}; bool has_ap_{false}; bool handled_connected_state_{false}; diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index b3167c5696..67b1f565ff 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -283,6 +283,12 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { if (!this->wifi_mode_(true, {})) return false; + // Check if the STA interface is initialized before using it + if (s_sta_netif == nullptr) { + ESP_LOGW(TAG, "STA interface not initialized"); + return false; + } + esp_netif_dhcp_status_t dhcp_status; esp_err_t err = esp_netif_dhcpc_get_status(s_sta_netif, &dhcp_status); if (err != ESP_OK) { @@ -474,8 +480,20 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Handshake Failed"; case WIFI_REASON_CONNECTION_FAIL: return "Connection Failed"; + case WIFI_REASON_AP_TSF_RESET: + return "AP TSF reset"; case WIFI_REASON_ROAMING: return "Station Roaming"; + case WIFI_REASON_ASSOC_COMEBACK_TIME_TOO_LONG: + return "Association comeback time too long"; + case WIFI_REASON_SA_QUERY_TIMEOUT: + return "SA query timeout"; + case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY: + return "No AP found with compatible security"; + case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD: + return "No AP found in auth mode threshold"; + case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD: + return "No AP found in RSSI threshold"; case WIFI_REASON_UNSPECIFIED: default: return "Unspecified"; @@ -529,6 +547,8 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_STOP: { ESP_LOGV(TAG, "STA stop"); + // Clear the STA interface handle to prevent use-after-free + s_sta_netif = nullptr; break; } case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { @@ -618,6 +638,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_AP_STOP: { ESP_LOGV(TAG, "AP stop"); +#ifdef USE_WIFI_AP + // Clear the AP interface handle to prevent use-after-free + s_ap_netif = nullptr; +#endif break; } case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { @@ -707,6 +731,12 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { if (!this->wifi_mode_({}, true)) return false; + // Check if the AP interface is initialized before using it + if (s_ap_netif == nullptr) { + ESP_LOGW(TAG, "AP interface not initialized"); + return false; + } + esp_netif_ip_info_t info; if (manual_ip.has_value()) { info.ip = manual_ip->static_ip; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index f0655a6d1d..94f1f5125f 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -473,6 +473,12 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { if (!this->wifi_mode_(true, {})) return false; + // Check if the STA interface is initialized before using it + if (s_sta_netif == nullptr) { + ESP_LOGW(TAG, "STA interface not initialized"); + return false; + } + esp_netif_dhcp_status_t dhcp_status; esp_err_t err = esp_netif_dhcpc_get_status(s_sta_netif, &dhcp_status); if (err != ESP_OK) { @@ -640,8 +646,20 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Handshake Failed"; case WIFI_REASON_CONNECTION_FAIL: return "Connection Failed"; + case WIFI_REASON_AP_TSF_RESET: + return "AP TSF reset"; case WIFI_REASON_ROAMING: return "Station Roaming"; + case WIFI_REASON_ASSOC_COMEBACK_TIME_TOO_LONG: + return "Association comeback time too long"; + case WIFI_REASON_SA_QUERY_TIMEOUT: + return "SA query timeout"; + case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY: + return "No AP found with compatible security"; + case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD: + return "No AP found in auth mode threshold"; + case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD: + return "No AP found in RSSI threshold"; case WIFI_REASON_UNSPECIFIED: default: return "Unspecified"; @@ -679,6 +697,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { ESP_LOGV(TAG, "STA stop"); s_sta_started = false; + // Clear the STA interface handle to prevent use-after-free + s_sta_netif = nullptr; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) { const auto &it = data->data.sta_authmode_change; @@ -777,6 +797,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STOP) { ESP_LOGV(TAG, "AP stop"); s_ap_started = false; +#ifdef USE_WIFI_AP + // Clear the AP interface handle to prevent use-after-free + s_ap_netif = nullptr; +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) { const auto &it = data->data.ap_probe_req_rx; @@ -853,6 +877,12 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { if (!this->wifi_mode_({}, true)) return false; + // Check if the AP interface is initialized before using it + if (s_ap_netif == nullptr) { + ESP_LOGW(TAG, "AP interface not initialized"); + return false; + } + esp_netif_ip_info_t info; if (manual_ip.has_value()) { info.ip = manual_ip->static_ip; 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 4efcf13e08..2de6f0d2e3 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -28,8 +28,6 @@ static const char *const LOGMSG_ONLINE = "online"; static const char *const LOGMSG_OFFLINE = "offline"; void Wireguard::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->wg_config_.address = this->address_.c_str(); this->wg_config_.private_key = this->private_key_.c_str(); this->wg_config_.endpoint = this->peer_endpoint_.c_str(); diff --git a/esphome/components/x9c/x9c.cpp b/esphome/components/x9c/x9c.cpp index ccd0c60b50..5cd4fba8c0 100644 --- a/esphome/components/x9c/x9c.cpp +++ b/esphome/components/x9c/x9c.cpp @@ -34,8 +34,6 @@ void X9cOutput::trim_value(int change_amount) { } void X9cOutput::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - this->inc_pin_->get_pin(); this->inc_pin_->setup(); this->inc_pin_->digital_write(false); diff --git a/esphome/components/xgzp68xx/xgzp68xx.cpp b/esphome/components/xgzp68xx/xgzp68xx.cpp index 52933ebdef..20a97cd04b 100644 --- a/esphome/components/xgzp68xx/xgzp68xx.cpp +++ b/esphome/components/xgzp68xx/xgzp68xx.cpp @@ -69,7 +69,6 @@ void XGZP68XXComponent::update() { } void XGZP68XXComponent::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); uint8_t config; // Display some sample bits to confirm we are talking to the sensor diff --git a/esphome/components/xl9535/xl9535.cpp b/esphome/components/xl9535/xl9535.cpp index 7bcd98070f..958fc5eede 100644 --- a/esphome/components/xl9535/xl9535.cpp +++ b/esphome/components/xl9535/xl9535.cpp @@ -7,8 +7,6 @@ namespace xl9535 { static const char *const TAG = "xl9535"; void XL9535Component::setup() { - ESP_LOGCONFIG(TAG, "Running setup"); - // Check to see if the device can read from the register uint8_t port = 0; if (this->read_register(XL9535_INPUT_PORT_0_REGISTER, &port, 1) != i2c::ERROR_OK) { diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py new file mode 100644 index 0000000000..c698122030 --- /dev/null +++ b/esphome/components/zephyr/__init__.py @@ -0,0 +1,252 @@ +import os +from typing import 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_BOARD, + KEY_BOOTLOADER, + KEY_EXTRA_BUILD_FILES, + KEY_OVERLAY, + KEY_PM_STATIC, + KEY_PRJ_CONF, + KEY_USER, + KEY_ZEPHYR, + zephyr_ns, +) + +CODEOWNERS = ["@tomaszduda23"] +AUTO_LOAD = ["preferences"] + +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] + user: dict[str, list[str]] + + +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=[], + user={}, + ) + 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 zephyr_add_user(key, value): + user = zephyr_data()[KEY_USER] + if key not in user: + user[key] = [] + user[key] += [value] + + +def copy_files(): + user = zephyr_data()[KEY_USER] + if user: + zephyr_add_overlay( + f""" +/ {{ + zephyr,user {{ + {[f"{key} = {', '.join(value)};" for key, value in user.items()][0]} +}}; +}};""" + ) + + 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..06a4fc42bc --- /dev/null +++ b/esphome/components/zephyr/const.py @@ -0,0 +1,16 @@ +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" +KEY_BOARD: Final = "board" +KEY_USER: Final = "user" + +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.py b/esphome/config.py index c4aa9aea24..90325cbf6e 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -198,10 +198,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): self.output_paths.remove((path, domain)) def is_in_error_path(self, path: ConfigPath) -> bool: - for err in self.errors: - if _path_begins_with(err.path, path): - return True - return False + return any(_path_begins_with(err.path, path) for err in self.errors) def set_by_path(self, path, value): conf = self @@ -224,7 +221,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): for index, path_item in enumerate(path): try: if path_item in data: - key_data = [x for x in data.keys() if x == path_item][0] + key_data = [x for x in data if x == path_item][0] if isinstance(key_data, ESPHomeDataBase): doc_range = key_data.esp_range if get_key and index == len(path) - 1: @@ -332,6 +329,28 @@ class ConfigValidationStep(abc.ABC): def run(self, result: Config) -> None: ... # noqa: E704 +class LoadTargetPlatformValidationStep(ConfigValidationStep): + """Load target platform step.""" + + def __init__(self, domain: str, conf: ConfigType): + self.domain = domain + self.conf = conf + + def run(self, result: Config) -> None: + if self.conf is None: + result[self.domain] = self.conf = {} + result.add_output_path([self.domain], self.domain) + component = get_component(self.domain) + + result[self.domain] = self.conf + path = [self.domain] + CORE.loaded_integrations.add(self.domain) + + result.add_validation_step( + SchemaValidationStep(self.domain, path, self.conf, component) + ) + + class LoadValidationStep(ConfigValidationStep): """Load step, this step is called once for each domain config fragment. @@ -585,16 +604,18 @@ class MetadataValidationStep(ConfigValidationStep): ) return for i, part_conf in enumerate(self.conf): + path = self.path + [i] result.add_validation_step( - SchemaValidationStep( - self.domain, self.path + [i], part_conf, self.comp - ) + SchemaValidationStep(self.domain, path, part_conf, self.comp) ) + result.add_validation_step(FinalValidateValidationStep(path, self.comp)) + return result.add_validation_step( SchemaValidationStep(self.domain, self.path, self.conf, self.comp) ) + result.add_validation_step(FinalValidateValidationStep(self.path, self.comp)) class SchemaValidationStep(ConfigValidationStep): @@ -606,13 +627,15 @@ class SchemaValidationStep(ConfigValidationStep): def __init__( self, domain: str, path: ConfigPath, conf: ConfigType, comp: ComponentManifest ): + self.domain = domain self.path = path self.conf = conf self.comp = comp def run(self, result: Config) -> None: token = path_context.set(self.path) - with result.catch_error(self.path): + # The domain already contains the full component path (e.g., "sensor.template", "sensor.uptime") + with CORE.component_context(self.domain), result.catch_error(self.path): if self.comp.is_platform: # Remove 'platform' key for validation input_conf = OrderedDict(self.conf) @@ -631,7 +654,6 @@ class SchemaValidationStep(ConfigValidationStep): result.set_by_path(self.path, validated) path_context.reset(token) - result.add_validation_step(FinalValidateValidationStep(self.path, self.comp)) class IDPassValidationStep(ConfigValidationStep): @@ -912,7 +934,7 @@ def validate_config( # First run platform validation steps result.add_validation_step( - LoadValidationStep(target_platform, config[target_platform]) + LoadTargetPlatformValidationStep(target_platform, config[target_platform]) ) result.run_validation_steps() @@ -920,6 +942,9 @@ def validate_config( # do not try to validate further as we don't know what the target is return result + # Reset the pin registry so that any target platforms with pin validations do not get the duplicate pin warning. + pins.PIN_SCHEMA_REGISTRY.reset() + for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) @@ -1081,7 +1106,7 @@ def dump_dict( ret += "{}" multiline = False - for k in conf.keys(): + for k in conf: path_ = path + [k] error = config.get_error_for_path(path_) if error is not None: @@ -1097,10 +1122,7 @@ def dump_dict( msg = f"\n{indent(msg)}" if inf is not None: - if m: - msg = f" {inf}{msg}" - else: - msg = f"{msg} {inf}" + msg = f" {inf}{msg}" if m else f"{msg} {inf}" ret += f"{st + msg}\n" elif isinstance(conf, str): if is_secret(conf): diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index 50ce4e8e34..00cd8f9818 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -111,8 +111,7 @@ def merge_config(full_old, full_new): else: ids[new_id] = len(res) res.append(v) - res = [v for i, v in enumerate(res) if i not in ids_to_delete] - return res + return [v for i, v in enumerate(res) if i not in ids_to_delete] if new is None: return old diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1c524cab6b..9aaeb9f9e8 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2,7 +2,7 @@ from __future__ import annotations -from contextlib import contextmanager +from contextlib import contextmanager, suppress from dataclasses import dataclass from datetime import datetime from ipaddress import ( @@ -87,7 +87,7 @@ from esphome.core import ( TimePeriodNanoseconds, TimePeriodSeconds, ) -from esphome.helpers import add_class_to_obj, list_starts_with +from esphome.helpers import add_class_to_obj, docs_url, list_starts_with from esphome.schema_extractors import ( SCHEMA_EXTRACT, schema_extractor, @@ -291,6 +291,8 @@ class Version: extra: str = "" def __str__(self): + if self.extra: + return f"{self.major}.{self.minor}.{self.patch}-{self.extra}" return f"{self.major}.{self.minor}.{self.patch}" @classmethod @@ -664,14 +666,6 @@ def only_with_framework( if suggestions is None: suggestions = {} - version = Version.parse(ESPHOME_VERSION) - if version.is_beta: - docs_format = "https://beta.esphome.io/components/{path}" - elif version.is_dev: - docs_format = "https://next.esphome.io/components/{path}" - else: - docs_format = "https://esphome.io/components/{path}" - def validator_(obj): if CORE.target_framework not in frameworks: err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}" @@ -679,7 +673,7 @@ def only_with_framework( (component, docs_path) = suggestion err_str += f"\nPlease use '{component}'" if docs_path: - err_str += f": {docs_format.format(path=docs_path)}" + err_str += f": {docs_url(path=f'components/{docs_path}')}" raise Invalid(err_str) return obj @@ -1107,6 +1101,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)?") @@ -1865,7 +1860,7 @@ def validate_registry_entry(name, registry): def none(value): if value in ("none", "None"): - return None + return raise Invalid("Must be none") @@ -2112,10 +2107,8 @@ def require_esphome_version(year, month, patch): @contextmanager def suppress_invalid(): - try: + with suppress(vol.Invalid): yield - except vol.Invalid: - pass GIT_SCHEMA = Schema( diff --git a/esphome/const.py b/esphome/const.py index ca6504d0c9..841630df5c 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.5" +__version__ = "2025.8.0b1" 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 ThreadModel(StrEnum): + """Threading model identifiers for ESPHome scheduler.""" + + SINGLE = "ESPHOME_THREAD_SINGLE" + MULTI_NO_ATOMICS = "ESPHOME_THREAD_MULTI_NO_ATOMICS" + MULTI_ATOMICS = "ESPHOME_THREAD_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" @@ -506,6 +523,7 @@ CONF_LOADED_INTEGRATIONS = "loaded_integrations" CONF_LOCAL = "local" CONF_LOCK_ACTION = "lock_action" CONF_LOG = "log" +CONF_LOG_LEVEL = "log_level" CONF_LOG_TOPIC = "log_topic" CONF_LOGGER = "logger" CONF_LOGS = "logs" @@ -743,6 +761,7 @@ CONF_POSITION_COMMAND_TOPIC = "position_command_topic" CONF_POSITION_STATE_TOPIC = "position_state_topic" CONF_POWER = "power" CONF_POWER_FACTOR = "power_factor" +CONF_POWER_MODE = "power_mode" CONF_POWER_ON_VALUE = "power_on_value" CONF_POWER_SAVE_MODE = "power_save_mode" CONF_POWER_SUPPLY = "power_supply" @@ -915,6 +934,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 +1207,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" @@ -1311,3 +1332,7 @@ ENTITY_CATEGORY_CONFIG = "config" # The entity category for read only diagnostic values, for example RSSI, uptime or MAC Address ENTITY_CATEGORY_DIAGNOSTIC = "diagnostic" + +# The corresponding constant exists in c++ +# when update_interval is set to never, it becomes SCHEDULER_DONT_RUN milliseconds +SCHEDULER_DONT_RUN = 4294967295 diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index e33bbcf726..9df5da1c78 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -1,4 +1,5 @@ from collections import defaultdict +from contextlib import contextmanager import logging import math import os @@ -21,6 +22,7 @@ from esphome.const import ( PLATFORM_ESP8266, PLATFORM_HOST, PLATFORM_LN882X, + PLATFORM_NRF52, PLATFORM_RP2040, PLATFORM_RTL87XX, ) @@ -37,7 +39,7 @@ from esphome.util import OrderedDict if TYPE_CHECKING: from ..cpp_generator import MockObj, MockObjClass, Statement - from ..types import ConfigType + from ..types import ConfigType, EntityMetadata _LOGGER = logging.getLogger(__name__) @@ -262,7 +264,7 @@ class TimePeriodMinutes(TimePeriod): pass -LAMBDA_PROG = re.compile(r"id\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)") +LAMBDA_PROG = re.compile(r"\bid\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)") class Lambda: @@ -469,6 +471,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 +552,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 @@ -524,14 +572,16 @@ class EsphomeCore: # Key: platform name (e.g. "sensor", "binary_sensor"), Value: count self.platform_counts: defaultdict[str, int] = defaultdict(int) # Track entity unique IDs to handle duplicates - # Set of (device_id, platform, sanitized_name) tuples - self.unique_ids: set[tuple[str, str, str]] = set() + # Dict mapping (device_id, platform, sanitized_name) -> entity metadata + self.unique_ids: dict[tuple[str, str, str], EntityMetadata] = {} # Whether ESPHome was started in verbose mode self.verbose = False # Whether ESPHome was started in quiet mode self.quiet = False # A list of all known ID classes self.id_classes = {} + # The current component being processed during validation + self.current_component: str | None = None def reset(self): from esphome.pins import PIN_SCHEMA_REGISTRY @@ -549,7 +599,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() @@ -557,9 +607,20 @@ class EsphomeCore: self.loaded_integrations = set() self.component_ids = set() self.platform_counts = defaultdict(int) - self.unique_ids = set() + self.unique_ids = {} + self.current_component = None PIN_SCHEMA_REGISTRY.reset() + @contextmanager + def component_context(self, component: str): + """Context manager to set the current component being processed.""" + old_component = self.current_component + self.current_component = component + try: + yield + finally: + self.current_component = old_component + @property def address(self) -> str | None: if self.config is None: @@ -670,6 +731,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 +751,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 +798,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 68bcc99d31..73bf13ab7c 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" @@ -31,6 +34,44 @@ namespace esphome { static const char *const TAG = "app"; +// Helper function for insertion sort of components by setup priority +// Using insertion sort instead of std::stable_sort saves ~1.3KB of flash +// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) +// IMPORTANT: This sort is stable (preserves relative order of equal elements), +// which is necessary to maintain user-defined component order for same priority +template static void insertion_sort_by_setup_priority(Iterator first, Iterator last) { + for (auto it = first + 1; it != last; ++it) { + auto key = *it; + float key_priority = key->get_actual_setup_priority(); + auto j = it - 1; + + // Using '<' (not '<=') ensures stability - equal priority components keep their order + while (j >= first && (*j)->get_actual_setup_priority() < key_priority) { + *(j + 1) = *j; + j--; + } + *(j + 1) = key; + } +} + +// Helper function for insertion sort of components by loop priority +// IMPORTANT: This sort is stable (preserves relative order of equal elements), +// which is required when components are re-sorted during setup() if they block +template static void insertion_sort_by_loop_priority(Iterator first, Iterator last) { + for (auto it = first + 1; it != last; ++it) { + auto key = *it; + float key_priority = key->get_loop_priority(); + auto j = it - 1; + + // Using '<' (not '<=') ensures stability - equal priority components keep their order + while (j >= first && (*j)->get_loop_priority() < key_priority) { + *(j + 1) = *j; + j--; + } + *(j + 1) = key; + } +} + void Application::register_component_(Component *comp) { if (comp == nullptr) { ESP_LOGW(TAG, "Tried to register null component!"); @@ -48,9 +89,12 @@ void Application::register_component_(Component *comp) { void Application::setup() { ESP_LOGI(TAG, "Running through setup()"); ESP_LOGV(TAG, "Sorting components by setup priority"); - std::stable_sort(this->components_.begin(), this->components_.end(), [](const Component *a, const Component *b) { - return a->get_actual_setup_priority() > b->get_actual_setup_priority(); - }); + + // Sort by setup priority using our helper function + insertion_sort_by_setup_priority(this->components_.begin(), this->components_.end()); + + // Initialize looping_components_ early so enable_pending_loops_() works during setup + this->calculate_looping_components_(); for (uint32_t i = 0; i < this->components_.size(); i++) { Component *component = this->components_[i]; @@ -63,8 +107,8 @@ void Application::setup() { if (component->can_proceed()) continue; - std::stable_sort(this->components_.begin(), this->components_.begin() + i + 1, - [](Component *a, Component *b) { return a->get_loop_priority() > b->get_loop_priority(); }); + // Sort components 0 through i by loop priority + insertion_sort_by_loop_priority(this->components_.begin(), this->components_.begin() + i + 1); do { uint8_t new_app_state = STATUS_LED_WARNING; @@ -94,7 +138,6 @@ void Application::setup() { clear_setup_priority_overrides(); this->schedule_dump_config(); - this->calculate_looping_components_(); } void Application::loop() { uint8_t new_app_state = 0; @@ -126,6 +169,14 @@ void Application::loop() { this->after_loop_tasks_(); this->app_state_ = new_app_state; +#ifdef USE_RUNTIME_STATS + // Process any pending runtime stats printing after all components have run + // This ensures stats printing doesn't affect component timing measurements + if (global_runtime_stats != nullptr) { + global_runtime_stats->process_pending_stats(last_op_end_time); + } +#endif + // Use the last component's end time instead of calling millis() again auto elapsed = last_op_end_time - this->last_loop_; if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { @@ -134,7 +185,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); @@ -258,7 +309,9 @@ void Application::calculate_looping_components_() { // Pre-reserve vector to avoid reallocations this->looping_components_.reserve(total_looping); - // First add all active components + // Add all components with loop override that aren't already LOOP_DONE + // Some components (like logger) may call disable_loop() during initialization + // before setup runs, so we need to respect their LOOP_DONE state for (auto *obj : this->components_) { if (obj->has_overridden_loop() && (obj->get_component_state() & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { @@ -268,8 +321,8 @@ void Application::calculate_looping_components_() { this->looping_components_active_end_ = this->looping_components_.size(); - // Then add all inactive (LOOP_DONE) components - // This handles components that called disable_loop() during setup, before this method runs + // Then add any components that are already LOOP_DONE to the inactive section + // This handles components that called disable_loop() during initialization for (auto *obj : this->components_) { if (obj->has_overridden_loop() && (obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { @@ -387,7 +440,7 @@ void Application::enable_pending_loops_() { void Application::before_loop_tasks_(uint32_t loop_start_time) { // Process scheduled tasks - this->scheduler.call(); + this->scheduler.call(loop_start_time); // Feed the watchdog timer this->feed_wdt(loop_start_time); @@ -444,24 +497,25 @@ void Application::unregister_socket_fd(int fd) { if (fd < 0) return; - auto it = std::find(this->socket_fds_.begin(), this->socket_fds_.end(), fd); - if (it != this->socket_fds_.end()) { + for (size_t i = 0; i < this->socket_fds_.size(); i++) { + if (this->socket_fds_[i] != fd) + continue; + // Swap with last element and pop - O(1) removal since order doesn't matter - if (it != this->socket_fds_.end() - 1) { - std::swap(*it, this->socket_fds_.back()); - } + if (i < this->socket_fds_.size() - 1) + this->socket_fds_[i] = this->socket_fds_.back(); this->socket_fds_.pop_back(); this->socket_fds_changed_ = true; // Only recalculate max_fd if we removed the current max if (fd == this->max_fd_) { - if (this->socket_fds_.empty()) { - this->max_fd_ = -1; - } else { - // Find new max using std::max_element - this->max_fd_ = *std::max_element(this->socket_fds_.begin(), this->socket_fds_.end()); + this->max_fd_ = -1; + for (int sock_fd : this->socket_fds_) { + if (sock_fd > this->max_fd_) + this->max_fd_ = sock_fd; } } + return; } } diff --git a/esphome/core/application.h b/esphome/core/application.h index f675881a27..4120afff53 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -101,12 +101,9 @@ class Application { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { - this->name_ = name + "-" + get_mac_address().substr(6); - if (friendly_name.empty()) { - this->friendly_name_ = ""; - } else { - this->friendly_name_ = friendly_name + " " + get_mac_address().substr(6); - } + const std::string mac_suffix = get_mac_address().substr(6); + this->name_ = name + "-" + mac_suffix; + this->friendly_name_ = friendly_name.empty() ? "" : friendly_name + " " + mac_suffix; } else { this->name_ = name; this->friendly_name_ = friendly_name; @@ -214,77 +211,6 @@ class Application { #endif /// Reserve space for components to avoid memory fragmentation - void reserve_components(size_t count) { this->components_.reserve(count); } - -#ifdef USE_BINARY_SENSOR - void reserve_binary_sensor(size_t count) { this->binary_sensors_.reserve(count); } -#endif -#ifdef USE_SWITCH - void reserve_switch(size_t count) { this->switches_.reserve(count); } -#endif -#ifdef USE_BUTTON - void reserve_button(size_t count) { this->buttons_.reserve(count); } -#endif -#ifdef USE_SENSOR - void reserve_sensor(size_t count) { this->sensors_.reserve(count); } -#endif -#ifdef USE_TEXT_SENSOR - void reserve_text_sensor(size_t count) { this->text_sensors_.reserve(count); } -#endif -#ifdef USE_FAN - void reserve_fan(size_t count) { this->fans_.reserve(count); } -#endif -#ifdef USE_COVER - void reserve_cover(size_t count) { this->covers_.reserve(count); } -#endif -#ifdef USE_CLIMATE - void reserve_climate(size_t count) { this->climates_.reserve(count); } -#endif -#ifdef USE_LIGHT - void reserve_light(size_t count) { this->lights_.reserve(count); } -#endif -#ifdef USE_NUMBER - void reserve_number(size_t count) { this->numbers_.reserve(count); } -#endif -#ifdef USE_DATETIME_DATE - void reserve_date(size_t count) { this->dates_.reserve(count); } -#endif -#ifdef USE_DATETIME_TIME - void reserve_time(size_t count) { this->times_.reserve(count); } -#endif -#ifdef USE_DATETIME_DATETIME - void reserve_datetime(size_t count) { this->datetimes_.reserve(count); } -#endif -#ifdef USE_SELECT - void reserve_select(size_t count) { this->selects_.reserve(count); } -#endif -#ifdef USE_TEXT - void reserve_text(size_t count) { this->texts_.reserve(count); } -#endif -#ifdef USE_LOCK - void reserve_lock(size_t count) { this->locks_.reserve(count); } -#endif -#ifdef USE_VALVE - void reserve_valve(size_t count) { this->valves_.reserve(count); } -#endif -#ifdef USE_MEDIA_PLAYER - void reserve_media_player(size_t count) { this->media_players_.reserve(count); } -#endif -#ifdef USE_ALARM_CONTROL_PANEL - void reserve_alarm_control_panel(size_t count) { this->alarm_control_panels_.reserve(count); } -#endif -#ifdef USE_EVENT - void reserve_event(size_t count) { this->events_.reserve(count); } -#endif -#ifdef USE_UPDATE - void reserve_update(size_t count) { this->updates_.reserve(count); } -#endif -#ifdef USE_AREAS - void reserve_area(size_t count) { this->areas_.reserve(count); } -#endif -#ifdef USE_DEVICES - void reserve_device(size_t count) { this->devices_.reserve(count); } -#endif /// Register the component in this Application instance. template C *register_component(C *c) { @@ -368,8 +294,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 auto &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,100 +315,95 @@ 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_; } + const auto &get_areas() { return this->areas_; } #endif #ifdef USE_BINARY_SENSOR - const std::vector &get_binary_sensors() { return this->binary_sensors_; } + auto &get_binary_sensors() const { return this->binary_sensors_; } GET_ENTITY_METHOD(binary_sensor::BinarySensor, binary_sensor, binary_sensors) #endif #ifdef USE_SWITCH - const std::vector &get_switches() { return this->switches_; } + auto &get_switches() const { return this->switches_; } GET_ENTITY_METHOD(switch_::Switch, switch, switches) #endif #ifdef USE_BUTTON - const std::vector &get_buttons() { return this->buttons_; } + auto &get_buttons() const { return this->buttons_; } GET_ENTITY_METHOD(button::Button, button, buttons) #endif #ifdef USE_SENSOR - const std::vector &get_sensors() { return this->sensors_; } + auto &get_sensors() const { return this->sensors_; } GET_ENTITY_METHOD(sensor::Sensor, sensor, sensors) #endif #ifdef USE_TEXT_SENSOR - const std::vector &get_text_sensors() { return this->text_sensors_; } + auto &get_text_sensors() const { return this->text_sensors_; } GET_ENTITY_METHOD(text_sensor::TextSensor, text_sensor, text_sensors) #endif #ifdef USE_FAN - const std::vector &get_fans() { return this->fans_; } + auto &get_fans() const { return this->fans_; } GET_ENTITY_METHOD(fan::Fan, fan, fans) #endif #ifdef USE_COVER - const std::vector &get_covers() { return this->covers_; } + auto &get_covers() const { return this->covers_; } GET_ENTITY_METHOD(cover::Cover, cover, covers) #endif #ifdef USE_LIGHT - const std::vector &get_lights() { return this->lights_; } + auto &get_lights() const { return this->lights_; } GET_ENTITY_METHOD(light::LightState, light, lights) #endif #ifdef USE_CLIMATE - const std::vector &get_climates() { return this->climates_; } + auto &get_climates() const { return this->climates_; } GET_ENTITY_METHOD(climate::Climate, climate, climates) #endif #ifdef USE_NUMBER - const std::vector &get_numbers() { return this->numbers_; } + auto &get_numbers() const { return this->numbers_; } GET_ENTITY_METHOD(number::Number, number, numbers) #endif #ifdef USE_DATETIME_DATE - const std::vector &get_dates() { return this->dates_; } + auto &get_dates() const { return this->dates_; } GET_ENTITY_METHOD(datetime::DateEntity, date, dates) #endif #ifdef USE_DATETIME_TIME - const std::vector &get_times() { return this->times_; } + auto &get_times() const { return this->times_; } GET_ENTITY_METHOD(datetime::TimeEntity, time, times) #endif #ifdef USE_DATETIME_DATETIME - const std::vector &get_datetimes() { return this->datetimes_; } + auto &get_datetimes() const { return this->datetimes_; } GET_ENTITY_METHOD(datetime::DateTimeEntity, datetime, datetimes) #endif #ifdef USE_TEXT - const std::vector &get_texts() { return this->texts_; } + auto &get_texts() const { return this->texts_; } GET_ENTITY_METHOD(text::Text, text, texts) #endif #ifdef USE_SELECT - const std::vector &get_selects() { return this->selects_; } + auto &get_selects() const { return this->selects_; } GET_ENTITY_METHOD(select::Select, select, selects) #endif #ifdef USE_LOCK - const std::vector &get_locks() { return this->locks_; } + auto &get_locks() const { return this->locks_; } GET_ENTITY_METHOD(lock::Lock, lock, locks) #endif #ifdef USE_VALVE - const std::vector &get_valves() { return this->valves_; } + auto &get_valves() const { return this->valves_; } GET_ENTITY_METHOD(valve::Valve, valve, valves) #endif #ifdef USE_MEDIA_PLAYER - const std::vector &get_media_players() { return this->media_players_; } + auto &get_media_players() const { return this->media_players_; } GET_ENTITY_METHOD(media_player::MediaPlayer, media_player, media_players) #endif #ifdef USE_ALARM_CONTROL_PANEL - const std::vector &get_alarm_control_panels() { - return this->alarm_control_panels_; - } + auto &get_alarm_control_panels() const { return this->alarm_control_panels_; } GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels) #endif #ifdef USE_EVENT - const std::vector &get_events() { return this->events_; } + auto &get_events() const { return this->events_; } GET_ENTITY_METHOD(event::Event, event, events) #endif #ifdef USE_UPDATE - const std::vector &get_updates() { return this->updates_; } + auto &get_updates() const { return this->updates_; } GET_ENTITY_METHOD(update::UpdateEntity, update, updates) #endif @@ -519,12 +451,7 @@ class Application { const char *comment_{nullptr}; const char *compilation_time_{nullptr}; - // size_t members - size_t dump_config_at_{SIZE_MAX}; - - // Vectors (largest members) - std::vector components_{}; - + // std::vector (3 pointers each: begin, end, capacity) // Partitioned vector design for looping components // ================================================= // Components are partitioned into [active | inactive] sections: @@ -542,85 +469,17 @@ class Application { // and active_end_ is incremented // - This eliminates branch mispredictions from flag checking in the hot loop std::vector looping_components_{}; - -#ifdef USE_DEVICES - std::vector devices_{}; -#endif -#ifdef USE_AREAS - std::vector areas_{}; -#endif -#ifdef USE_BINARY_SENSOR - std::vector binary_sensors_{}; -#endif -#ifdef USE_SWITCH - std::vector switches_{}; -#endif -#ifdef USE_BUTTON - std::vector buttons_{}; -#endif -#ifdef USE_EVENT - std::vector events_{}; -#endif -#ifdef USE_SENSOR - std::vector sensors_{}; -#endif -#ifdef USE_TEXT_SENSOR - std::vector text_sensors_{}; -#endif -#ifdef USE_FAN - std::vector fans_{}; -#endif -#ifdef USE_COVER - std::vector covers_{}; -#endif -#ifdef USE_CLIMATE - std::vector climates_{}; -#endif -#ifdef USE_LIGHT - std::vector lights_{}; -#endif -#ifdef USE_NUMBER - std::vector numbers_{}; -#endif -#ifdef USE_DATETIME_DATE - std::vector dates_{}; -#endif -#ifdef USE_DATETIME_TIME - std::vector times_{}; -#endif -#ifdef USE_DATETIME_DATETIME - std::vector datetimes_{}; -#endif -#ifdef USE_SELECT - std::vector selects_{}; -#endif -#ifdef USE_TEXT - std::vector texts_{}; -#endif -#ifdef USE_LOCK - std::vector locks_{}; -#endif -#ifdef USE_VALVE - std::vector valves_{}; -#endif -#ifdef USE_MEDIA_PLAYER - std::vector media_players_{}; -#endif -#ifdef USE_ALARM_CONTROL_PANEL - std::vector alarm_control_panels_{}; -#endif -#ifdef USE_UPDATE - std::vector updates_{}; -#endif - #ifdef USE_SOCKET_SELECT_SUPPORT std::vector socket_fds_; // Vector of all monitored socket file descriptors #endif - // String members + // std::string members (typically 24-32 bytes each) std::string name_; std::string friendly_name_; + // size_t members + size_t dump_config_at_{SIZE_MAX}; + // 4-byte members uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; @@ -630,9 +489,9 @@ class Application { #endif // 2-byte members (grouped together for alignment) - uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) - uint16_t looping_components_active_end_{0}; - uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration + uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds) + uint16_t looping_components_active_end_{0}; // Index marking end of active components in looping_components_ + uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration // 1-byte members (grouped together to minimize padding) uint8_t app_state_{0}; @@ -642,11 +501,87 @@ class Application { #ifdef USE_SOCKET_SELECT_SUPPORT bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes +#endif - // Variable-sized members at end +#ifdef USE_SOCKET_SELECT_SUPPORT + // Variable-sized members fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_ #endif + + // StaticVectors (largest members - contain actual array data inline) + StaticVector components_{}; + +#ifdef USE_DEVICES + StaticVector devices_{}; +#endif +#ifdef USE_AREAS + StaticVector areas_{}; +#endif +#ifdef USE_BINARY_SENSOR + StaticVector binary_sensors_{}; +#endif +#ifdef USE_SWITCH + StaticVector switches_{}; +#endif +#ifdef USE_BUTTON + StaticVector buttons_{}; +#endif +#ifdef USE_EVENT + StaticVector events_{}; +#endif +#ifdef USE_SENSOR + StaticVector sensors_{}; +#endif +#ifdef USE_TEXT_SENSOR + StaticVector text_sensors_{}; +#endif +#ifdef USE_FAN + StaticVector fans_{}; +#endif +#ifdef USE_COVER + StaticVector covers_{}; +#endif +#ifdef USE_CLIMATE + StaticVector climates_{}; +#endif +#ifdef USE_LIGHT + StaticVector lights_{}; +#endif +#ifdef USE_NUMBER + StaticVector numbers_{}; +#endif +#ifdef USE_DATETIME_DATE + StaticVector dates_{}; +#endif +#ifdef USE_DATETIME_TIME + StaticVector times_{}; +#endif +#ifdef USE_DATETIME_DATETIME + StaticVector datetimes_{}; +#endif +#ifdef USE_SELECT + StaticVector selects_{}; +#endif +#ifdef USE_TEXT + StaticVector texts_{}; +#endif +#ifdef USE_LOCK + StaticVector locks_{}; +#endif +#ifdef USE_VALVE + StaticVector valves_{}; +#endif +#ifdef USE_MEDIA_PLAYER + StaticVector media_players_{}; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + StaticVector + alarm_control_panels_{}; +#endif +#ifdef USE_UPDATE + StaticVector updates_{}; +#endif }; /// Global storage of Application pointer - only one Application can exist. diff --git a/esphome/core/color.h b/esphome/core/color.h index 2b307bb438..5dce58a485 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -1,8 +1,13 @@ #pragma once +#include "defines.h" #include "component.h" #include "helpers.h" +#ifdef USE_LVGL +#include "esphome/components/lvgl/lvgl_proxy.h" +#endif // USE_LVGL + namespace esphome { inline static constexpr uint8_t esp_scale8(uint8_t i, uint8_t scale) { @@ -33,6 +38,11 @@ struct Color { uint32_t raw_32; }; +#ifdef USE_LVGL + // convenience function for Color to get a lv_color_t representation + operator lv_color_t() const { return lv_color_make(this->r, this->g, this->b); } +#endif + inline constexpr Color() ESPHOME_ALWAYS_INLINE : raw_32(0) {} // NOLINT inline constexpr Color(uint8_t red, uint8_t green, uint8_t blue) ESPHOME_ALWAYS_INLINE : r(red), g(green), diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 800fbcaa28..40cda17ca3 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -9,10 +9,14 @@ #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 { static const char *const TAG = "component"; +static const char *const UNSPECIFIED_MESSAGE = "unspecified"; // Global vectors for component data that doesn't belong in every instance. // Using vector instead of unordered_map for both because: @@ -129,7 +133,7 @@ void Component::call_dump_config() { this->dump_config(); if (this->is_failed()) { // Look up error message from global vector - const char *error_msg = "unspecified"; + const char *error_msg = nullptr; if (component_error_messages) { for (const auto &pair : *component_error_messages) { if (pair.first == this) { @@ -138,7 +142,8 @@ void Component::call_dump_config() { } } } - ESP_LOGE(TAG, " %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 ? error_msg : UNSPECIFIED_MESSAGE); } } @@ -146,28 +151,33 @@ uint8_t Component::get_component_state() const { return this->component_state_; void Component::call() { uint8_t state = this->component_state_ & COMPONENT_STATE_MASK; switch (state) { - case COMPONENT_STATE_CONSTRUCTION: + case COMPONENT_STATE_CONSTRUCTION: { // State Construction: Call setup and set state to setup - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_SETUP; + this->set_component_state_(COMPONENT_STATE_SETUP); + ESP_LOGV(TAG, "Setup %s", this->get_component_source()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + uint32_t start_time = millis(); +#endif this->call_setup(); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + uint32_t setup_time = millis() - start_time; + ESP_LOGCONFIG(TAG, "Setup %s took %ums", this->get_component_source(), (unsigned) setup_time); +#endif break; + } case COMPONENT_STATE_SETUP: // State setup: Call first loop and set state to loop - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_LOOP; + this->set_component_state_(COMPONENT_STATE_LOOP); this->call_loop(); break; case COMPONENT_STATE_LOOP: // State loop: Call loop this->call_loop(); break; - case COMPONENT_STATE_FAILED: // NOLINT(bugprone-branch-clone) + case COMPONENT_STATE_FAILED: // State failed: Do nothing - break; - case COMPONENT_STATE_LOOP_DONE: // NOLINT(bugprone-branch-clone) + case COMPONENT_STATE_LOOP_DONE: // State loop done: Do nothing, component has finished its work - break; default: break; } @@ -192,25 +202,26 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) { } void Component::mark_failed() { 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->set_component_state_(COMPONENT_STATE_FAILED); this->status_set_error(); // Also remove from loop since failed components shouldn't loop App.disable_component_loop_(this); } +void Component::set_component_state_(uint8_t state) { + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= state; +} void Component::disable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { ESP_LOGVV(TAG, "%s loop disabled", this->get_component_source()); - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_LOOP_DONE; + this->set_component_state_(COMPONENT_STATE_LOOP_DONE); App.disable_component_loop_(this); } } void Component::enable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { ESP_LOGVV(TAG, "%s loop enabled", this->get_component_source()); - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_LOOP; + this->set_component_state_(COMPONENT_STATE_LOOP); App.enable_component_loop_(this); } } @@ -230,8 +241,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { ESP_LOGI(TAG, "%s is being reset to construction state", this->get_component_source()); - this->component_state_ &= ~COMPONENT_STATE_MASK; - this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; + this->set_component_state_(COMPONENT_STATE_CONSTRUCTION); // Clear error status when resetting this->status_clear_error(); } @@ -276,15 +286,15 @@ void Component::status_set_warning(const char *message) { return; this->component_state_ |= STATUS_LED_WARNING; App.app_state_ |= STATUS_LED_WARNING; - ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message); + ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message ? message : UNSPECIFIED_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, "%s set Error flag: %s", this->get_component_source(), message); - if (strcmp(message, "unspecified") != 0) { + ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message ? message : UNSPECIFIED_MESSAGE); + if (message != nullptr) { // Lazy allocate the error messages vector if needed if (!component_error_messages) { component_error_messages = std::make_unique>>(); @@ -370,11 +380,10 @@ bool Component::has_overridden_loop() const { PollingComponent::PollingComponent(uint32_t update_interval) : update_interval_(update_interval) {} void PollingComponent::call_setup() { + // init the poller before calling setup, allowing setup to cancel it if desired + this->start_poller(); // Let the polling component subclass setup their HW. this->setup(); - - // init the poller - this->start_poller(); } void PollingComponent::start_poller() { @@ -396,6 +405,13 @@ uint32_t WarnIfComponentBlockingGuard::finish() { uint32_t curr_time = millis(); uint32_t blocking_time = curr_time - this->started_; + +#ifdef USE_RUNTIME_STATS + // Record component runtime stats + if (global_runtime_stats != nullptr) { + global_runtime_stats->record_component_time(this->component_, blocking_time, curr_time); + } +#endif bool should_warn; if (this->component_ != nullptr) { should_warn = this->component_->should_warn_of_blocking(blocking_time); diff --git a/esphome/core/component.h b/esphome/core/component.h index 3734473a02..096c6f9c69 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -202,9 +202,9 @@ class Component { bool status_has_error() const; - void status_set_warning(const char *message = "unspecified"); + void status_set_warning(const char *message = nullptr); - void status_set_error(const char *message = "unspecified"); + void status_set_error(const char *message = nullptr); void status_clear_warning(); @@ -236,6 +236,9 @@ class Component { virtual void call_setup(); virtual void call_dump_config(); + /// Helper to set component state (clears state bits and sets new state) + void set_component_state_(uint8_t state); + /** Set an interval function with a unique name. Empty name means no cancelling possible. * * This will call f every interval ms. Can be cancelled via CancelInterval(). @@ -405,10 +408,10 @@ class Component { const char *component_source_{nullptr}; uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) /// State of this component - each bit has a purpose: - /// Bits 0-1: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED) - /// Bit 2: STATUS_LED_WARNING - /// Bit 3: STATUS_LED_ERROR - /// Bits 4-7: Unused - reserved for future expansion (50% of the bits are free) + /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) + /// Bit 3: STATUS_LED_WARNING + /// Bit 4: STATUS_LED_ERROR + /// Bits 5-7: Unused - reserved for future expansion uint8_t component_state_{0x00}; volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context }; diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index d27c4e70ba..668c4a1fda 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -16,373 +16,173 @@ void ComponentIterator::begin(bool include_internal) { this->at_ = 0; this->include_internal_ = include_internal; } + +void ComponentIterator::advance_platform_() { + this->state_ = static_cast(static_cast(this->state_) + 1); + this->at_ = 0; +} + void ComponentIterator::advance() { - bool advance_platform = false; - bool success = true; switch (this->state_) { case IteratorState::NONE: // not started return; case IteratorState::BEGIN: if (this->on_begin()) { - advance_platform = true; - } else { - return; + advance_platform_(); } break; + #ifdef USE_BINARY_SENSOR case IteratorState::BINARY_SENSOR: - if (this->at_ >= App.get_binary_sensors().size()) { - advance_platform = true; - } else { - auto *binary_sensor = App.get_binary_sensors()[this->at_]; - if (binary_sensor->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_binary_sensor(binary_sensor); - } - } + this->process_platform_item_(App.get_binary_sensors(), &ComponentIterator::on_binary_sensor); break; #endif + #ifdef USE_COVER case IteratorState::COVER: - if (this->at_ >= App.get_covers().size()) { - advance_platform = true; - } else { - auto *cover = App.get_covers()[this->at_]; - if (cover->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_cover(cover); - } - } + this->process_platform_item_(App.get_covers(), &ComponentIterator::on_cover); break; #endif + #ifdef USE_FAN case IteratorState::FAN: - if (this->at_ >= App.get_fans().size()) { - advance_platform = true; - } else { - auto *fan = App.get_fans()[this->at_]; - if (fan->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_fan(fan); - } - } + this->process_platform_item_(App.get_fans(), &ComponentIterator::on_fan); break; #endif + #ifdef USE_LIGHT case IteratorState::LIGHT: - if (this->at_ >= App.get_lights().size()) { - advance_platform = true; - } else { - auto *light = App.get_lights()[this->at_]; - if (light->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_light(light); - } - } + this->process_platform_item_(App.get_lights(), &ComponentIterator::on_light); break; #endif + #ifdef USE_SENSOR case IteratorState::SENSOR: - if (this->at_ >= App.get_sensors().size()) { - advance_platform = true; - } else { - auto *sensor = App.get_sensors()[this->at_]; - if (sensor->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_sensor(sensor); - } - } + this->process_platform_item_(App.get_sensors(), &ComponentIterator::on_sensor); break; #endif + #ifdef USE_SWITCH case IteratorState::SWITCH: - if (this->at_ >= App.get_switches().size()) { - advance_platform = true; - } else { - auto *a_switch = App.get_switches()[this->at_]; - if (a_switch->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_switch(a_switch); - } - } + this->process_platform_item_(App.get_switches(), &ComponentIterator::on_switch); break; #endif + #ifdef USE_BUTTON case IteratorState::BUTTON: - if (this->at_ >= App.get_buttons().size()) { - advance_platform = true; - } else { - auto *button = App.get_buttons()[this->at_]; - if (button->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_button(button); - } - } + this->process_platform_item_(App.get_buttons(), &ComponentIterator::on_button); break; #endif + #ifdef USE_TEXT_SENSOR case IteratorState::TEXT_SENSOR: - if (this->at_ >= App.get_text_sensors().size()) { - advance_platform = true; - } else { - auto *text_sensor = App.get_text_sensors()[this->at_]; - if (text_sensor->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_text_sensor(text_sensor); - } - } + this->process_platform_item_(App.get_text_sensors(), &ComponentIterator::on_text_sensor); break; #endif + #ifdef USE_API_SERVICES - case IteratorState ::SERVICE: - if (this->at_ >= api::global_api_server->get_user_services().size()) { - advance_platform = true; - } else { - auto *service = api::global_api_server->get_user_services()[this->at_]; - success = this->on_service(service); - } + case IteratorState::SERVICE: + this->process_platform_item_(api::global_api_server->get_user_services(), &ComponentIterator::on_service); break; #endif + #ifdef USE_CAMERA - case IteratorState::CAMERA: - if (camera::Camera::instance() == nullptr) { - advance_platform = true; - } else { - if (camera::Camera::instance()->is_internal() && !this->include_internal_) { - advance_platform = success = true; - break; - } else { - advance_platform = success = this->on_camera(camera::Camera::instance()); - } + case IteratorState::CAMERA: { + camera::Camera *camera_instance = camera::Camera::instance(); + if (camera_instance != nullptr && (!camera_instance->is_internal() || this->include_internal_)) { + this->on_camera(camera_instance); } - break; + advance_platform_(); + } break; #endif + #ifdef USE_CLIMATE case IteratorState::CLIMATE: - if (this->at_ >= App.get_climates().size()) { - advance_platform = true; - } else { - auto *climate = App.get_climates()[this->at_]; - if (climate->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_climate(climate); - } - } + this->process_platform_item_(App.get_climates(), &ComponentIterator::on_climate); break; #endif + #ifdef USE_NUMBER case IteratorState::NUMBER: - if (this->at_ >= App.get_numbers().size()) { - advance_platform = true; - } else { - auto *number = App.get_numbers()[this->at_]; - if (number->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_number(number); - } - } + this->process_platform_item_(App.get_numbers(), &ComponentIterator::on_number); break; #endif + #ifdef USE_DATETIME_DATE case IteratorState::DATETIME_DATE: - if (this->at_ >= App.get_dates().size()) { - advance_platform = true; - } else { - auto *date = App.get_dates()[this->at_]; - if (date->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_date(date); - } - } + this->process_platform_item_(App.get_dates(), &ComponentIterator::on_date); break; #endif + #ifdef USE_DATETIME_TIME case IteratorState::DATETIME_TIME: - if (this->at_ >= App.get_times().size()) { - advance_platform = true; - } else { - auto *time = App.get_times()[this->at_]; - if (time->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_time(time); - } - } + this->process_platform_item_(App.get_times(), &ComponentIterator::on_time); break; #endif + #ifdef USE_DATETIME_DATETIME case IteratorState::DATETIME_DATETIME: - if (this->at_ >= App.get_datetimes().size()) { - advance_platform = true; - } else { - auto *datetime = App.get_datetimes()[this->at_]; - if (datetime->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_datetime(datetime); - } - } + this->process_platform_item_(App.get_datetimes(), &ComponentIterator::on_datetime); break; #endif + #ifdef USE_TEXT case IteratorState::TEXT: - if (this->at_ >= App.get_texts().size()) { - advance_platform = true; - } else { - auto *text = App.get_texts()[this->at_]; - if (text->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_text(text); - } - } + this->process_platform_item_(App.get_texts(), &ComponentIterator::on_text); break; #endif + #ifdef USE_SELECT case IteratorState::SELECT: - if (this->at_ >= App.get_selects().size()) { - advance_platform = true; - } else { - auto *select = App.get_selects()[this->at_]; - if (select->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_select(select); - } - } + this->process_platform_item_(App.get_selects(), &ComponentIterator::on_select); break; #endif + #ifdef USE_LOCK case IteratorState::LOCK: - if (this->at_ >= App.get_locks().size()) { - advance_platform = true; - } else { - auto *a_lock = App.get_locks()[this->at_]; - if (a_lock->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_lock(a_lock); - } - } + this->process_platform_item_(App.get_locks(), &ComponentIterator::on_lock); break; #endif + #ifdef USE_VALVE case IteratorState::VALVE: - if (this->at_ >= App.get_valves().size()) { - advance_platform = true; - } else { - auto *valve = App.get_valves()[this->at_]; - if (valve->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_valve(valve); - } - } + this->process_platform_item_(App.get_valves(), &ComponentIterator::on_valve); break; #endif + #ifdef USE_MEDIA_PLAYER case IteratorState::MEDIA_PLAYER: - if (this->at_ >= App.get_media_players().size()) { - advance_platform = true; - } else { - auto *media_player = App.get_media_players()[this->at_]; - if (media_player->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_media_player(media_player); - } - } + this->process_platform_item_(App.get_media_players(), &ComponentIterator::on_media_player); break; #endif + #ifdef USE_ALARM_CONTROL_PANEL case IteratorState::ALARM_CONTROL_PANEL: - if (this->at_ >= App.get_alarm_control_panels().size()) { - advance_platform = true; - } else { - auto *a_alarm_control_panel = App.get_alarm_control_panels()[this->at_]; - if (a_alarm_control_panel->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_alarm_control_panel(a_alarm_control_panel); - } - } + this->process_platform_item_(App.get_alarm_control_panels(), &ComponentIterator::on_alarm_control_panel); break; #endif + #ifdef USE_EVENT case IteratorState::EVENT: - if (this->at_ >= App.get_events().size()) { - advance_platform = true; - } else { - auto *event = App.get_events()[this->at_]; - if (event->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_event(event); - } - } + this->process_platform_item_(App.get_events(), &ComponentIterator::on_event); break; #endif + #ifdef USE_UPDATE case IteratorState::UPDATE: - if (this->at_ >= App.get_updates().size()) { - advance_platform = true; - } else { - auto *update = App.get_updates()[this->at_]; - if (update->is_internal() && !this->include_internal_) { - success = true; - break; - } else { - success = this->on_update(update); - } - } + this->process_platform_item_(App.get_updates(), &ComponentIterator::on_update); break; #endif + case IteratorState::MAX: if (this->on_end()) { this->state_ = IteratorState::NONE; } return; } - - if (advance_platform) { - this->state_ = static_cast(static_cast(this->state_) + 1); - this->at_ = 0; - } else if (success) { - this->at_++; - } } + bool ComponentIterator::on_end() { return true; } bool ComponentIterator::on_begin() { return true; } #ifdef USE_API_SERVICES diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index ea2c8004ac..fdc30485bc 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -171,6 +171,21 @@ 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 Container &items, + bool (ComponentIterator::*on_item)(typename Container::value_type)) { + if (this->at_ >= items.size()) { + this->advance_platform_(); + } else { + typename Container::value_type item = items[this->at_]; + if ((item->is_internal() && !this->include_internal_) || (this->*on_item)(item)) { + this->at_++; + } + } + } + + void advance_platform_(); }; } // namespace esphome diff --git a/esphome/core/config.py b/esphome/core/config.py index f73369f28f..90768a4b09 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -317,7 +317,7 @@ def preload_core_config(config, result) -> str: target_platforms = [] - for domain, _ in config.items(): + for domain in config: if domain.startswith("."): continue if _is_target_platform(domain): @@ -419,10 +419,27 @@ async def _add_automations(config): await automation.build_automation(trigger, [], conf) +# Datetime component has special subtypes that need additional defines +DATETIME_SUBTYPES = {"date", "time", "datetime"} + + @coroutine_with_priority(-100.0) -async def _add_platform_reserves() -> None: +async def _add_platform_defines() -> None: + # Generate compile-time defines for platforms that have actual entities + # Only add USE_* and count defines when there are entities for platform_name, count in sorted(CORE.platform_counts.items()): - cg.add(cg.RawStatement(f"App.reserve_{platform_name}({count});"), prepend=True) + if count <= 0: + continue + + define_name = f"ESPHOME_ENTITY_{platform_name.upper()}_COUNT" + cg.add_define(define_name, count) + + # Datetime subtypes only use USE_DATETIME_* defines + if platform_name in DATETIME_SUBTYPES: + cg.add_define(f"USE_DATETIME_{platform_name.upper()}") + else: + # Regular platforms use USE_* defines + cg.add_define(f"USE_{platform_name.upper()}") @coroutine_with_priority(100.0) @@ -442,12 +459,10 @@ async def to_code(config: ConfigType) -> None: config[CONF_NAME_ADD_MAC_SUFFIX], ) ) - # Reserve space for components to avoid reallocation during registration - cg.add( - cg.RawStatement(f"App.reserve_components({len(CORE.component_ids)});"), - ) + # Define component count for static allocation + cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids)) - CORE.add_job(_add_platform_reserves) + CORE.add_job(_add_platform_defines) CORE.add_job(_add_automations, config) @@ -514,8 +529,8 @@ async def to_code(config: ConfigType) -> None: all_areas.extend(config[CONF_AREAS]) if all_areas: - cg.add(cg.RawStatement(f"App.reserve_area({len(all_areas)});")) cg.add_define("USE_AREAS") + cg.add_define("ESPHOME_AREA_COUNT", len(all_areas)) for area_conf in all_areas: area_id: core.ID = area_conf[CONF_ID] @@ -532,9 +547,9 @@ async def to_code(config: ConfigType) -> None: if not devices: return - # Reserve space for devices - cg.add(cg.RawStatement(f"App.reserve_device({len(devices)});")) + # Define device count for static allocation cg.add_define("USE_DEVICES") + cg.add_define("ESPHOME_DEVICE_COUNT", len(devices)) # Process each device for dev_conf in devices: diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8ed8f4b5aa..5df3bcf475 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-threaded with atomics) +#define ESPHOME_THREAD_MULTI_ATOMICS + // logger #define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_VERY_VERBOSE @@ -106,6 +109,8 @@ #define USE_API #define USE_API_CLIENT_CONNECTED_TRIGGER #define USE_API_CLIENT_DISCONNECTED_TRIGGER +#define USE_API_HOMEASSISTANT_SERVICES +#define USE_API_HOMEASSISTANT_STATES #define USE_API_NOISE #define USE_API_PLAINTEXT #define USE_API_SERVICES @@ -142,10 +147,15 @@ #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_BLUETOOTH_PROXY +#define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 +#define BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE 16 #define USE_CAPTIVE_PORTAL #define USE_ESP32_BLE #define USE_ESP32_BLE_CLIENT +#define USE_ESP32_BLE_DEVICE #define USE_ESP32_BLE_SERVER +#define USE_ESP32_BLE_UUID +#define USE_ESP32_BLE_ADVERTISING #define USE_I2C #define USE_IMPROV #define USE_MICROPHONE @@ -156,18 +166,18 @@ #define USE_SPI #define USE_VOICE_ASSISTANT #define USE_WEBSERVER +#define USE_WEBSERVER_AUTH #define USE_WEBSERVER_OTA #define USE_WEBSERVER_PORT 80 // NOLINT #define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT #ifdef USE_ARDUINO -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 3) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1) #define USE_ETHERNET #endif #ifdef USE_ESP_IDF -#define USE_ESP_IDF_VERSION_CODE VERSION_CODE(5, 3, 2) #define USE_MICRO_WAKE_WORD #define USE_MICRO_WAKE_WORD_VAD #if defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) @@ -204,6 +214,7 @@ {} #define USE_WEBSERVER +#define USE_WEBSERVER_AUTH #define USE_WEBSERVER_PORT 80 // NOLINT #endif @@ -220,6 +231,7 @@ #define USE_SOCKET_IMPL_LWIP_SOCKETS #define USE_SOCKET_SELECT_SUPPORT #define USE_WEBSERVER +#define USE_WEBSERVER_AUTH #define USE_WEBSERVER_PORT 80 // NOLINT #endif @@ -233,3 +245,29 @@ // #define USE_BSEC2 // Requires a library with proprietary license #define USE_DASHBOARD_IMPORT + +// Default counts for static analysis +#define ESPHOME_COMPONENT_COUNT 50 +#define ESPHOME_DEVICE_COUNT 10 +#define ESPHOME_AREA_COUNT 10 +#define ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT 1 +#define ESPHOME_ENTITY_BINARY_SENSOR_COUNT 1 +#define ESPHOME_ENTITY_BUTTON_COUNT 1 +#define ESPHOME_ENTITY_CLIMATE_COUNT 1 +#define ESPHOME_ENTITY_COVER_COUNT 1 +#define ESPHOME_ENTITY_DATE_COUNT 1 +#define ESPHOME_ENTITY_DATETIME_COUNT 1 +#define ESPHOME_ENTITY_EVENT_COUNT 1 +#define ESPHOME_ENTITY_FAN_COUNT 1 +#define ESPHOME_ENTITY_LIGHT_COUNT 1 +#define ESPHOME_ENTITY_LOCK_COUNT 1 +#define ESPHOME_ENTITY_MEDIA_PLAYER_COUNT 1 +#define ESPHOME_ENTITY_NUMBER_COUNT 1 +#define ESPHOME_ENTITY_SELECT_COUNT 1 +#define ESPHOME_ENTITY_SENSOR_COUNT 1 +#define ESPHOME_ENTITY_SWITCH_COUNT 1 +#define ESPHOME_ENTITY_TEXT_COUNT 1 +#define ESPHOME_ENTITY_TEXT_SENSOR_COUNT 1 +#define ESPHOME_ENTITY_TIME_COUNT 1 +#define ESPHOME_ENTITY_UPDATE_COUNT 1 +#define ESPHOME_ENTITY_VALVE_COUNT 1 diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 00b1264ed0..e60e0728bc 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -54,6 +54,14 @@ class EntityBase { // Get/set this entity's icon std::string get_icon() const; void set_icon(const char *icon); + StringRef get_icon_ref() const { + static constexpr auto EMPTY_STRING = StringRef::from_lit(""); +#ifdef USE_ENTITY_ICON + return this->icon_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->icon_c_str_); +#else + return EMPTY_STRING; +#endif + } #ifdef USE_DEVICES // Get/set this entity's device id @@ -105,6 +113,11 @@ class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) std::string get_device_class(); /// Manually set the device class. void set_device_class(const char *device_class); + /// Get the device class as StringRef + StringRef get_device_class_ref() const { + static constexpr auto EMPTY_STRING = StringRef::from_lit(""); + return this->device_class_ == nullptr ? EMPTY_STRING : StringRef(this->device_class_); + } protected: const char *device_class_{nullptr}; ///< Device class override @@ -116,6 +129,11 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) std::string get_unit_of_measurement(); /// Manually set the unit of measurement. void set_unit_of_measurement(const char *unit_of_measurement); + /// Get the unit of measurement as StringRef + StringRef get_unit_of_measurement_ref() const { + static constexpr auto EMPTY_STRING = StringRef::from_lit(""); + return this->unit_of_measurement_ == nullptr ? EMPTY_STRING : StringRef(this->unit_of_measurement_); + } protected: const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 5ad16ac76c..107b9fd739 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -16,7 +16,7 @@ from esphome.core import CORE, ID from esphome.cpp_generator import MockObj, add, get_variable import esphome.final_validate as fv from esphome.helpers import sanitize, snake_case -from esphome.types import ConfigType +from esphome.types import ConfigType, EntityMetadata _LOGGER = logging.getLogger(__name__) @@ -198,9 +198,12 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy # Get device name if entity is on a sub-device device_name = None + device_id = "" # Empty string for main device if CONF_DEVICE_ID in config: device_id_obj = config[CONF_DEVICE_ID] device_name = device_id_obj.id + # Use the device ID string directly for uniqueness + device_id = device_id_obj.id # Calculate what object_id will actually be used # This handles empty names correctly by using device/friendly names @@ -209,15 +212,47 @@ 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: + # Get the existing entity metadata + existing = CORE.unique_ids[unique_key] + existing_name = existing.get("name", entity_name) + existing_device = existing.get("device_id", "") + existing_id = existing.get("entity_id", "unknown") + + # Build detailed error message + device_prefix = f" on device '{device_id}'" if device_id else "" + existing_device_prefix = ( + f" on device '{existing_device}'" if existing_device else "" + ) + existing_component = existing.get("component", "unknown") + + # Provide more context about where the duplicate was found + conflict_msg = ( + f"Conflicts with entity '{existing_name}'{existing_device_prefix}" + ) + if existing_id != "unknown": + conflict_msg += f" (id: {existing_id})" + if existing_component != "unknown": + conflict_msg += f" from component '{existing_component}'" + 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"{conflict_msg}. " + f"Each entity on a device must have a unique name within its platform." ) - # Add to tracking set - CORE.unique_ids.add(unique_key) + # Store metadata about this entity + entity_metadata: EntityMetadata = { + "name": entity_name, + "device_id": device_id, + "platform": platform, + "entity_id": str(config.get(CONF_ID, "unknown")), + "component": CORE.current_component or "unknown", + } + + # Add to tracking dict + CORE.unique_ids[unique_key] = entity_metadata return config return validator diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index b46077af02..e84f5a7317 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -258,7 +258,9 @@ 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; @@ -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); } @@ -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() - (separator ? 1 : 0)); - 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 4a4c52bb06..b5fe59c4fd 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1,9 +1,11 @@ #pragma once +#include #include #include #include #include +#include #include #include #include @@ -90,6 +92,51 @@ template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n); ///@} +/// @name Container utilities +///@{ + +/// Minimal static vector - saves memory by avoiding std::vector overhead +template class StaticVector { + public: + using value_type = T; + using iterator = typename std::array::iterator; + using const_iterator = typename std::array::const_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + private: + std::array data_{}; + size_t count_{0}; + + public: + // Minimal vector-compatible interface - only what we actually use + void push_back(const T &value) { + if (count_ < N) { + data_[count_++] = value; + } + } + + size_t size() const { return count_; } + bool empty() const { return count_ == 0; } + + T &operator[](size_t i) { return data_[i]; } + const T &operator[](size_t i) const { return data_[i]; } + + // For range-based for loops + iterator begin() { return data_.begin(); } + iterator end() { return data_.begin() + count_; } + const_iterator begin() const { return data_.begin(); } + const_iterator end() const { return data_.begin() + count_; } + + // Reverse iterators + reverse_iterator rbegin() { return reverse_iterator(end()); } + reverse_iterator rend() { return reverse_iterator(begin()); } + const_reverse_iterator rbegin() const { return const_reverse_iterator(end()); } + const_reverse_iterator rend() const { return const_reverse_iterator(begin()); } +}; + +///@} + /// @name Mathematics ///@{ @@ -580,21 +627,28 @@ template class CallbackManager { /// Helper class to deduplicate items in a series of values. template class Deduplicator { public: - /// Feeds the next item in the series to the deduplicator and returns whether this is a duplicate. + /// Feeds the next item in the series to the deduplicator and returns false if this is a duplicate. bool next(T value) { - if (this->has_value_) { - if (this->last_value_ == value) - return false; + if (this->has_value_ && !this->value_unknown_ && this->last_value_ == value) { + return false; } this->has_value_ = true; + this->value_unknown_ = false; this->last_value_ = value; return true; } - /// Returns whether this deduplicator has processed any items so far. + /// Returns true if the deduplicator's value was previously known. + bool next_unknown() { + bool ret = !this->value_unknown_; + this->value_unknown_ = true; + return ret; + } + /// Returns true if this deduplicator has processed any items. bool has_value() const { return this->has_value_; } protected: bool has_value_{false}; + bool value_unknown_{false}; T last_value_{}; }; @@ -681,7 +735,7 @@ 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 }; diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index de07b0ebba..68e2825d09 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -29,7 +29,7 @@ namespace esphome { // Base lock-free queue without task notification template class LockFreeQueue { public: - LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} + LockFreeQueue() : dropped_count_(0), head_(0), tail_(0) {} bool push(T *element) { bool was_empty; diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 8a31e4f42e..6269a66543 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -8,12 +8,17 @@ #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; +// max delay to start an interval sequence +static constexpr uint32_t MAX_INTERVAL_DELAY = 5000; // Uncomment to debug scheduler // #define ESPHOME_DEBUG_SCHEDULER @@ -51,7 +56,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, @@ -60,7 +65,7 @@ static void validate_static_string(const char *name) { // Common implementation for both timeout and interval void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, - const void *name_ptr, uint32_t delay, std::function func) { + const void *name_ptr, uint32_t delay, std::function func, bool is_retry) { // Get the name as const char* const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); @@ -78,10 +83,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->type = type; item->callback = std::move(func); item->remove = false; + item->is_retry = is_retry; -#if !defined(USE_ESP8266) && !defined(USE_RP2040) +#ifndef ESPHOME_THREAD_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_}; @@ -89,16 +95,20 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type this->defer_queue_.push_back(std::move(item)); return; } -#endif +#endif /* not ESPHOME_THREAD_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) { item->interval = delay; - // Calculate random offset (0 to interval/2) - uint32_t offset = (delay != 0) ? (random_uint32() % delay) / 2 : 0; + // first execution happens immediately after a random smallish offset + // Calculate random offset (0 to min(interval/2, 5s)) + uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); item->next_execution_ = now + offset; + ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr ? name_cstr : "", delay, + offset); } else { item->interval = 0; item->next_execution_ = now + delay; @@ -119,9 +129,21 @@ 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_}; + + // For retries, check if there's a cancelled timeout first + if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT && + (has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) || + has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { + // Skip scheduling - the retry was cancelled +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr); +#endif + return; + } + // If name is provided, do atomic cancel-and-add // Cancel existing items this->cancel_item_locked_(component, name_cstr, type); @@ -170,32 +192,34 @@ struct RetryArgs { Scheduler *scheduler; }; -static void retry_handler(const std::shared_ptr &args) { +void retry_handler(const std::shared_ptr &args) { RetryResult const retry_result = args->func(--args->retry_countdown); if (retry_result == RetryResult::DONE || args->retry_countdown <= 0) return; // second execution of `func` happens after `initial_wait_time` - args->scheduler->set_timeout(args->component, args->name, args->current_interval, [args]() { retry_handler(args); }); + args->scheduler->set_timer_common_( + args->component, Scheduler::SchedulerItem::TIMEOUT, false, &args->name, args->current_interval, + [args]() { retry_handler(args); }, /* is_retry= */ true); // backoff_increase_factor applied to third & later executions args->current_interval *= args->backoff_increase_factor; } -void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, - uint8_t max_attempts, std::function func, - float backoff_increase_factor) { - if (!name.empty()) - this->cancel_retry(component, name); +void HOT Scheduler::set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, + uint32_t initial_wait_time, uint8_t max_attempts, + std::function func, float backoff_increase_factor) { + const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); + + if (name_cstr != nullptr) + this->cancel_retry(component, name_cstr); if (initial_wait_time == SCHEDULER_DONT_RUN) return; ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%" PRIu32 ", max_attempts=%u, backoff_factor=%0.1f)", - name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor); + name_cstr ? name_cstr : "", 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_cstr ? name_cstr : ""); backoff_increase_factor = 1; } @@ -204,31 +228,56 @@ void HOT Scheduler::set_retry(Component *component, const std::string &name, uin args->retry_countdown = max_attempts; args->current_interval = initial_wait_time; args->component = component; - args->name = "retry$" + name; + args->name = name_cstr ? name_cstr : ""; // Convert to std::string for RetryArgs args->backoff_increase_factor = backoff_increase_factor; args->scheduler = this; - // First execution of `func` immediately - this->set_timeout(component, args->name, 0, [args]() { retry_handler(args); }); -} -bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) { - return this->cancel_timeout(component, "retry$" + name); + // First execution of `func` immediately - use set_timer_common_ with is_retry=true + this->set_timer_common_( + component, SchedulerItem::TIMEOUT, false, &args->name, 0, [args]() { retry_handler(args); }, + /* is_retry= */ true); } -optional HOT Scheduler::next_schedule_in() { - // IMPORTANT: This method should only be called from the main thread (loop task). - // It calls empty_() and accesses items_[0] without holding a lock, which is only - // safe when called from the main thread. Other threads must not call this method. - if (this->empty_()) - return {}; - auto &item = this->items_[0]; - const auto now = this->millis_(); - if (item->next_execution_ < now) - return 0; - return item->next_execution_ - now; +void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, + uint8_t max_attempts, std::function func, + float backoff_increase_factor) { + this->set_retry_common_(component, false, &name, initial_wait_time, max_attempts, std::move(func), + backoff_increase_factor); } -void HOT Scheduler::call() { -#if !defined(USE_ESP8266) && !defined(USE_RP2040) + +void HOT Scheduler::set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts, + std::function func, float backoff_increase_factor) { + this->set_retry_common_(component, true, name, initial_wait_time, max_attempts, std::move(func), + backoff_increase_factor); +} +bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) { + return this->cancel_retry(component, name.c_str()); +} + +bool HOT Scheduler::cancel_retry(Component *component, const char *name) { + // Cancel timeouts that have is_retry flag set + LockGuard guard{this->lock_}; + return this->cancel_item_locked_(component, name, SchedulerItem::TIMEOUT, /* match_retry= */ true); +} + +optional HOT Scheduler::next_schedule_in(uint32_t now) { + // IMPORTANT: This method should only be called from the main thread (loop task). + // 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 no items, return empty optional + if (this->cleanup_() == 0) + return {}; + + auto &item = this->items_[0]; + // Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit + const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from caller + if (item->next_execution_ < now_64) + return 0; + return item->next_execution_ - now_64; +} +void HOT Scheduler::call(uint32_t now) { +#ifndef ESPHOME_THREAD_SINGLE // Process defer queue first to guarantee FIFO execution order for deferred items. // Previously, defer() used the heap which gave undefined order for equal timestamps, // causing race conditions on multi-core systems (ESP32, BK7200). @@ -236,8 +285,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 @@ -256,23 +304,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_THREAD_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_THREAD_MULTI_ATOMICS + const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); + const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, + major_dbg, last_dbg); +#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ + ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, + this->millis_major_, this->last_millis_); +#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ + // Cleanup before debug output + this->cleanup_(); + while (!this->items_.empty()) { std::unique_ptr item; { LockGuard guard{this->lock_}; @@ -283,7 +341,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)); } @@ -296,7 +354,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) { @@ -323,12 +381,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; } @@ -342,13 +402,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); } { @@ -367,7 +427,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)); @@ -389,8 +449,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 @@ -398,7 +458,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 @@ -412,10 +472,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); @@ -423,11 +484,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(); } @@ -444,7 +503,8 @@ 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) { +bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type, + bool match_retry) { // Early return if name is invalid - no items to cancel if (name_cstr == nullptr) { return false; @@ -453,21 +513,21 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c size_t total_cancelled = 0; // Check all containers for matching items -#if !defined(USE_ESP8266) && !defined(USE_RP2040) +#ifndef ESPHOME_THREAD_SINGLE // Only check defer queue for timeouts (intervals never go there) if (type == SchedulerItem::TIMEOUT) { for (auto &item : this->defer_queue_) { - if (this->matches_item_(item, component, name_cstr, type)) { + if (this->matches_item_(item, component, name_cstr, type, match_retry)) { item->remove = true; total_cancelled++; } } } -#endif +#endif /* not ESPHOME_THREAD_SINGLE */ // Cancel items in the main heap for (auto &item : this->items_) { - if (this->matches_item_(item, component, name_cstr, type)) { + if (this->matches_item_(item, component, name_cstr, type, match_retry)) { item->remove = true; total_cancelled++; this->to_remove_++; // Track removals for heap items @@ -476,7 +536,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c // Cancel items in to_add_ for (auto &item : this->to_add_) { - if (this->matches_item_(item, component, name_cstr, type)) { + if (this->matches_item_(item, component, name_cstr, type, match_retry)) { item->remove = true; total_cancelled++; // Don't track removals for to_add_ items @@ -486,19 +546,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_THREAD_SINGLE - Runs on single-threaded platforms (ESP8266, RP2040, etc.) + // - ESPHOME_THREAD_MULTI_NO_ATOMICS - Runs on multi-threaded platforms without atomics (LibreTiny) + // - ESPHOME_THREAD_MULTI_ATOMICS - Runs on multi-threaded 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_THREAD_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_THREAD_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_THREAD_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_THREAD_SINGLE, ESPHOME_THREAD_MULTI_NO_ATOMICS, or ESPHOME_THREAD_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 a3da2c20f6..a6092e1b1e 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_THREAD_MULTI_ATOMICS +#include +#endif #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -11,8 +15,15 @@ namespace esphome { class Component; +struct RetryArgs; + +// Forward declaration of retry_handler - needs to be non-static for friend declaration +void retry_handler(const std::shared_ptr &args); class Scheduler { + // Allow retry_handler to access protected members + friend void ::esphome::retry_handler(const std::shared_ptr &args); + public: // Public API - accepts std::string for backward compatibility void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func); @@ -50,11 +61,21 @@ class Scheduler { bool cancel_interval(Component *component, const char *name); void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, std::function func, float backoff_increase_factor = 1.0f); + void set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts, + std::function func, float backoff_increase_factor = 1.0f); bool cancel_retry(Component *component, const std::string &name); + bool cancel_retry(Component *component, const char *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(); @@ -80,11 +101,18 @@ class Scheduler { enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; bool remove : 1; bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[]) - // 5 bits padding + bool is_retry : 1; // True if this is a retry timeout + // 4 bits padding // Constructor SchedulerItem() - : component(nullptr), interval(0), next_execution_(0), type(TIMEOUT), remove(false), name_is_dynamic(false) { + : component(nullptr), + interval(0), + next_execution_(0), + type(TIMEOUT), + remove(false), + name_is_dynamic(false), + is_retry(false) { name_.static_name = nullptr; } @@ -136,15 +164,22 @@ class Scheduler { // Common implementation for both timeout and interval void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, - uint32_t delay, std::function func); + uint32_t delay, std::function func, bool is_retry = false); - uint64_t millis_(); - void cleanup_(); + // Common implementation for retry + void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time, + uint8_t max_attempts, std::function func, float backoff_increase_factor); + + 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: // Helper to cancel items by name - must be called with lock held - bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type); + bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type, bool match_retry = false); // Helper to extract name as const char* from either static string or std::string inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) { @@ -156,8 +191,9 @@ class Scheduler { // Helper function to check if item matches criteria for cancellation inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, - SchedulerItem::Type type) { - if (item->component != component || item->type != type || item->remove) { + SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { + if (item->component != component || item->type != type || (skip_removed && item->remove) || + (match_retry && !item->is_retry)) { return false; } const char *item_name = item->get_name(); @@ -176,37 +212,63 @@ 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(); + // Template helper to check if any item in a container matches our criteria + template + bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, + bool match_retry) const { + for (const auto &item : container) { + if (item->remove && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, + /* skip_removed= */ false)) { + return true; + } + } + return false; } 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_THREAD_SINGLE + // Single-core platforms don't need the defer queue and save 40 bytes of RAM std::deque> defer_queue_; // FIFO queue for defer() calls -#endif - uint32_t last_millis_{0}; - uint16_t millis_major_{0}; +#endif /* ESPHOME_THREAD_SINGLE */ uint32_t to_remove_{0}; + +#ifdef ESPHOME_THREAD_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_THREAD_MULTI_ATOMICS */ + // Platforms without atomic support or single-threaded platforms + uint32_t last_millis_{0}; +#endif /* else ESPHOME_THREAD_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_THREAD_MULTI_ATOMICS + std::atomic millis_major_{0}; +#else /* not ESPHOME_THREAD_MULTI_ATOMICS */ + uint16_t millis_major_{0}; +#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ }; } // namespace esphome diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 060dd36f8f..34e4eec1ee 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -771,8 +771,7 @@ class MockObj(Expression): if attr.startswith("P") and self.op not in ["::", ""]: attr = attr[1:] next_op = "->" - if attr.startswith("_"): - attr = attr[1:] + attr = attr.removeprefix("_") return MockObj(f"{self.base}{self.op}{attr}", next_op) def __call__(self, *args: SafeExpType) -> "MockObj": @@ -1037,10 +1036,7 @@ class MockObjClass(MockObj): def inherits_from(self, other: "MockObjClass") -> bool: if str(self) == str(other): return True - for parent in self._parents: - if str(parent) == str(other): - return True - return False + return any(str(parent) == str(other) for parent in self._parents) def template(self, *args: SafeExpType) -> "MockObjClass": if len(args) != 1 or not isinstance(args[0], TemplateArguments): diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 3f64be6154..b61b215bdc 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -115,7 +115,7 @@ async def build_registry_list(registry, config): async def past_safe_mode(): if CONF_SAFE_MODE not in CORE.config: - return + return None def _safe_mode_generator(): while True: diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 9de2d39ce2..81c10763e7 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from asyncio import events from concurrent.futures import ThreadPoolExecutor +import contextlib import logging import os import socket @@ -125,10 +126,8 @@ def start_dashboard(args) -> None: asyncio.set_event_loop_policy(DashboardEventLoopPolicy(settings.verbose)) - try: + with contextlib.suppress(KeyboardInterrupt): asyncio.run(async_start(args)) - except KeyboardInterrupt: - pass async def async_start(args) -> None: 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/dashboard/web_server.py b/esphome/dashboard/web_server.py index 480285b6c1..fd16667d8a 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -144,7 +144,7 @@ def websocket_class(cls): if not hasattr(cls, "_message_handlers"): cls._message_handlers = {} - for _, method in cls.__dict__.items(): + for method in cls.__dict__.values(): if hasattr(method, "_message_handler"): cls._message_handlers[method._message_handler] = method @@ -229,6 +229,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + close_fds=False, ) stdout_thread = threading.Thread(target=self._stdout_thread) stdout_thread.daemon = True @@ -324,39 +325,53 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): configuration = json_message["configuration"] config_file = settings.rel_path(configuration) port = json_message["port"] + addresses: list[str] = [] if ( port == "OTA" # pylint: disable=too-many-boolean-expressions and (entry := entries.get(config_file)) and entry.loaded_integrations and "api" in entry.loaded_integrations ): - if (mdns := dashboard.mdns_status) and ( - address_list := await mdns.async_resolve_host(entry.name) - ): - # Use the IP address if available but only - # if the API is loaded and the device is online - # since MQTT logging will not work otherwise - port = sort_ip_addresses(address_list)[0] - elif ( - entry.address + # First priority: entry.address AKA use_address + if ( + (use_address := entry.address) and ( address_list := await dashboard.dns_cache.async_resolve( - entry.address, time.monotonic() + use_address, time.monotonic() ) ) and not isinstance(address_list, Exception) ): - # If mdns is not available, try to use the DNS cache - port = sort_ip_addresses(address_list)[0] + addresses.extend(sort_ip_addresses(address_list)) - return [ - *DASHBOARD_COMMAND, - *args, - config_file, - "--device", - port, + # Second priority: mDNS + if ( + (mdns := dashboard.mdns_status) + and (address_list := await mdns.async_resolve_host(entry.name)) + and ( + new_addresses := [ + addr for addr in address_list if addr not in addresses + ] + ) + ): + # Use the IP address if available but only + # if the API is loaded and the device is online + # since MQTT logging will not work otherwise + addresses.extend(sort_ip_addresses(new_addresses)) + + if not addresses: + # If no address was found, use the port directly + # as otherwise they will get the chooser which + # does not work with the dashboard as there is no + # interactive way to get keyboard input + addresses = [port] + + device_args: list[str] = [ + arg for address in addresses for arg in ("--device", address) ] + return [*DASHBOARD_COMMAND, *args, config_file, *device_args] + class EsphomeLogsHandler(EsphomePortCommandWebSocket): async def build_command(self, json_message: dict[str, Any]) -> list[str]: diff --git a/esphome/espota2.py b/esphome/espota2.py index 4f2a08fb4a..279bafee8e 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -88,10 +88,7 @@ def recv_decode(sock, amount, decode=True): def receive_exactly(sock, amount, msg, expect, decode=True): - if decode: - data = [] - else: - data = b"" + data = [] if decode else b"" try: data += recv_decode(sock, 1, decode=decode) diff --git a/esphome/git.py b/esphome/git.py index 005bcae702..56aedd1519 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -17,7 +17,9 @@ _LOGGER = logging.getLogger(__name__) def run_git_command(cmd, cwd=None) -> str: _LOGGER.debug("Running git command: %s", " ".join(cmd)) try: - ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) + ret = subprocess.run( + cmd, cwd=cwd, capture_output=True, check=False, close_fds=False + ) except FileNotFoundError as err: raise cv.Invalid( "git is not installed but required for external_components.\n" diff --git a/esphome/helpers.py b/esphome/helpers.py index bf0e3b5cf7..377a4e1717 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -9,6 +9,8 @@ import re import tempfile from urllib.parse import urlparse +from esphome.const import __version__ as ESPHOME_VERSION + _LOGGER = logging.getLogger(__name__) IS_MACOS = platform.system() == "Darwin" @@ -96,9 +98,7 @@ def cpp_string_escape(string, encoding="utf-8"): def _should_escape(byte: int) -> bool: if not 32 <= byte < 127: return True - if byte in (ord("\\"), ord('"')): - return True - return False + return byte in (ord("\\"), ord('"')) if isinstance(string, str): string = string.encode(encoding) @@ -114,7 +114,9 @@ def cpp_string_escape(string, encoding="utf-8"): def run_system_command(*args): import subprocess - with subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p: + with subprocess.Popen( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False + ) as p: stdout, stderr = p.communicate() rc = p.returncode return rc, stdout, stderr @@ -505,3 +507,20 @@ _DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9-_]") def sanitize(value): """Same behaviour as `helpers.cpp` method `str_sanitize`.""" return _DISALLOWED_CHARS.sub("_", value) + + +def docs_url(path: str) -> str: + """Return the URL to the documentation for a given path.""" + # Local import to avoid circular import + from esphome.config_validation import Version + + version = Version.parse(ESPHOME_VERSION) + if version.is_beta: + docs_format = "https://beta.esphome.io/{path}" + elif version.is_dev: + docs_format = "https://next.esphome.io/{path}" + else: + docs_format = "https://esphome.io/{path}" + + path = path.removeprefix("/") + return docs_format.format(path=path) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index c43b622684..687efd2b49 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -2,7 +2,7 @@ dependencies: espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: - version: 2.0.15 + version: 2.1.1 espressif/mdns: version: 1.8.2 espressif/esp_wifi_remote: diff --git a/esphome/log.py b/esphome/log.py index 0e91eb32c2..bfd1875b55 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -37,6 +37,8 @@ class AnsiStyle(Enum): def color(col: AnsiFore, msg: str, reset: bool = True) -> str: + if col == AnsiFore.KEEP: + return msg s = col.value + msg if reset and col: s += AnsiStyle.RESET_ALL.value @@ -61,7 +63,7 @@ class ESPHomeLogFormatter(logging.Formatter): }.get(record.levelname, "") message = f"{prefix}{formatted}{AnsiStyle.RESET_ALL.value}" if CORE.dashboard: - try: + try: # noqa: SIM105 message = message.replace("\033", "\\033") except UnicodeEncodeError: pass diff --git a/esphome/mqtt.py b/esphome/mqtt.py index a420b5ba7f..f1c631697a 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -1,3 +1,4 @@ +import contextlib from datetime import datetime import hashlib import json @@ -35,7 +36,7 @@ _LOGGER = logging.getLogger(__name__) def config_from_env(): - config = { + return { CONF_MQTT: { CONF_USERNAME: get_str_env("ESPHOME_DASHBOARD_MQTT_USERNAME"), CONF_PASSWORD: get_str_env("ESPHOME_DASHBOARD_MQTT_PASSWORD"), @@ -43,7 +44,6 @@ def config_from_env(): CONF_PORT: get_int_env("ESPHOME_DASHBOARD_MQTT_PORT", 1883), }, } - return config def initialize( @@ -52,10 +52,8 @@ def initialize( client = prepare( config, subscriptions, on_message, on_connect, username, password, client_id ) - try: + with contextlib.suppress(KeyboardInterrupt): client.loop_forever() - except KeyboardInterrupt: - pass return 0 diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index e34ac028f8..267277ebe1 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -61,6 +61,7 @@ FILTER_PLATFORMIO_LINES = [ r"Advanced Memory Usage is available via .*", r"Merged .* ELF section", r"esptool.py v.*", + r"esptool v.*", r"Checking size .*", r"Retrieving maximum program size .*", r"PLATFORM: .*", @@ -131,9 +132,11 @@ def _load_idedata(config): temp_idedata = Path(CORE.relative_internal_path("idedata", f"{CORE.name}.json")) changed = False - if not platformio_ini.is_file() or not temp_idedata.is_file(): - changed = True - elif platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime: + if ( + not platformio_ini.is_file() + or not temp_idedata.is_file() + or platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime + ): changed = True if not changed: @@ -208,7 +211,7 @@ def _decode_pc(config, addr): return command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr] try: - translation = subprocess.check_output(command).decode().strip() + translation = subprocess.check_output(command, close_fds=False).decode().strip() except Exception: # pylint: disable=broad-except _LOGGER.debug("Caught exception for command %s", command, exc_info=1) return diff --git a/esphome/types.py b/esphome/types.py index f68f503993..62499a953c 100644 --- a/esphome/types.py +++ b/esphome/types.py @@ -1,5 +1,7 @@ """This helper module tracks commonly used types in the esphome python codebase.""" +from typing import TypedDict + from esphome.core import ID, EsphomeCore, Lambda ConfigFragmentType = ( @@ -16,3 +18,13 @@ ConfigFragmentType = ( ConfigType = dict[str, ConfigFragmentType] CoreType = EsphomeCore ConfigPathType = str | int + + +class EntityMetadata(TypedDict): + """Metadata stored for each entity to help with duplicate detection.""" + + name: str + device_id: str + platform: str + entity_id: str + component: str diff --git a/esphome/util.py b/esphome/util.py index 79cb630200..6362260fde 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -6,6 +6,7 @@ from pathlib import Path import re import subprocess import sys +from typing import Any from esphome import const @@ -59,7 +60,7 @@ def safe_print(message="", end="\n"): from esphome.core import CORE if CORE.dashboard: - try: + try: # noqa: SIM105 message = message.replace("\033", "\\033") except UnicodeEncodeError: pass @@ -110,7 +111,7 @@ class RedirectText: def __getattr__(self, item): return getattr(self._out, item) - def _write_color_replace(self, s): + def _write_color_replace(self, s: str | bytes) -> None: from esphome.core import CORE if CORE.dashboard: @@ -121,7 +122,7 @@ class RedirectText: s = s.replace("\033", "\\033") self._out.write(s) - def write(self, s): + def write(self, s: str | bytes) -> int: # s is usually a str already (self._out is of type TextIOWrapper) # However, s is sometimes also a bytes object in python3. Let's make sure it's a # str @@ -223,7 +224,7 @@ def run_external_command( return retval -def run_external_process(*cmd, **kwargs): +def run_external_process(*cmd: str, **kwargs: Any) -> int | str: full_cmd = " ".join(shlex_quote(x) for x in cmd) _LOGGER.debug("Running: %s", full_cmd) filter_lines = kwargs.get("filter_lines") @@ -238,7 +239,12 @@ def run_external_process(*cmd, **kwargs): try: proc = subprocess.run( - cmd, stdout=sub_stdout, stderr=sub_stderr, encoding="utf-8", check=False + cmd, + stdout=sub_stdout, + stderr=sub_stderr, + encoding="utf-8", + check=False, + close_fds=False, ) return proc.stdout if capture_stdout else proc.returncode except KeyboardInterrupt: # pylint: disable=try-except-raise @@ -266,7 +272,7 @@ class OrderedDict(collections.OrderedDict): return dict(self).__repr__() -def list_yaml_files(folders): +def list_yaml_files(folders: list[str]) -> list[str]: files = filter_yaml_files( [os.path.join(folder, p) for folder in folders for p in os.listdir(folder)] ) @@ -274,7 +280,7 @@ def list_yaml_files(folders): return files -def filter_yaml_files(files): +def filter_yaml_files(files: list[str]) -> list[str]: return [ f for f in files @@ -345,5 +351,11 @@ def get_esp32_arduino_flash_error_help() -> str | None: + "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" + + "Some Arduino-specific libraries may need alternatives.\n" + + "\n" + + "For detailed migration instructions, see:\n" + + color( + AnsiFore.BLUE, + "https://esphome.io/guides/esp32_arduino_to_idf.html\n\n", + ) ) diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 8fb966e3b2..7220fb307f 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -225,9 +225,10 @@ class _Schema(vol.Schema): return ret schema = schemas[0] + extra_schemas = self._extra_schemas.copy() + if isinstance(schema, _Schema): + extra_schemas.extend(schema._extra_schemas) if isinstance(schema, vol.Schema): schema = schema.schema ret = super().extend(schema, extra=extra) - return _Schema( - ret.schema, extra=ret.extra, extra_schemas=self._extra_schemas.copy() - ) + return _Schema(ret.schema, extra=ret.extra, extra_schemas=extra_schemas) diff --git a/esphome/vscode.py b/esphome/vscode.py index d8cfe91938..f5e2a20b97 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -81,8 +81,7 @@ def _print_file_read_event(path: str) -> None: def _request_and_get_stream_on_stdin(fname: str) -> StringIO: _print_file_read_event(fname) - raw_yaml_stream = StringIO(_read_file_content_from_json_on_stdin()) - return raw_yaml_stream + return StringIO(_read_file_content_from_json_on_stdin()) def _vscode_loader(fname: str) -> dict[str, Any]: diff --git a/esphome/wizard.py b/esphome/wizard.py index 6f7cbd1ff4..8602e90222 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -116,10 +116,7 @@ def wizard_file(**kwargs): kwargs["fallback_name"] = ap_name kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12)) - if kwargs.get("friendly_name"): - base = BASE_CONFIG_FRIENDLY - else: - base = BASE_CONFIG + base = BASE_CONFIG_FRIENDLY if kwargs.get("friendly_name") else BASE_CONFIG config = base.format(**kwargs) diff --git a/esphome/writer.py b/esphome/writer.py index 5438e48570..b5c834722a 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, } @@ -95,7 +76,7 @@ def get_include_text(): def replace_file_content(text, pattern, repl): - content_new, count = re.subn(pattern, repl, text, flags=re.M) + content_new, count = re.subn(pattern, repl, text, flags=re.MULTILINE) return content_new, count @@ -105,21 +86,17 @@ def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool: if old.src_version != new.src_version: return True - if old.build_path != new.build_path: - return True - - return False + return old.build_path != new.build_path def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool: if ( old.loaded_integrations != new.loaded_integrations or old.loaded_platforms != new.loaded_platforms - ): - if new.core_platform == PLATFORM_ESP32: - from esphome.components.esp32 import FRAMEWORK_ESP_IDF + ) and new.core_platform == PLATFORM_ESP32: + from esphome.components.esp32 import FRAMEWORK_ESP_IDF - return new.framework == FRAMEWORK_ESP_IDF + return new.framework == FRAMEWORK_ESP_IDF return False @@ -140,40 +117,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)) - - # 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 find_begin_end(text, begin_s, end_s): begin_index = text.find(begin_s) if begin_index == -1: @@ -201,34 +144,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) - - # Write extra script for C++ specific flags - write_cxx_flags_script() - - DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ #pragma once #include "esphome/core/macros.h" @@ -400,20 +315,3 @@ def write_gitignore(): if not os.path.isfile(path): with open(file=path, mode="w", encoding="utf-8") as f: f.write(GITIGNORE_CONTENT) - - -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/yaml_util.py b/esphome/yaml_util.py index e52fc9e788..f26bc0502d 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -56,9 +56,12 @@ class ESPHomeDataBase: def from_node(self, node): # pylint: disable=attribute-defined-outside-init self._esp_range = DocumentRange.from_marks(node.start_mark, node.end_mark) - if isinstance(node, yaml.ScalarNode): - if node.style is not None and node.style in "|>": - self._content_offset = 1 + if ( + isinstance(node, yaml.ScalarNode) + and node.style is not None + and node.style in "|>" + ): + self._content_offset = 1 def from_database(self, database): # pylint: disable=attribute-defined-outside-init @@ -302,8 +305,7 @@ class ESPHomeLoaderMixin: result = self.yaml_loader(self._rel_path(file)) if not vars: vars = {} - result = substitute_vars(result, vars) - return result + return substitute_vars(result, vars) @_add_data_ref def construct_include_dir_list(self, node: yaml.Node) -> list[dict[str, Any]]: diff --git a/platformio.ini b/platformio.ini index 7fb301c08b..d9f2f879ec 100644 --- a/platformio.ini +++ b/platformio.ini @@ -40,6 +40,7 @@ lib_deps = functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 pavlodn/HaierProtocol@0.9.31 ; haier kikuchan98/pngle@1.1.0 ; online_image + https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps ; Using the repository directly, otherwise ESP-IDF can't use the library https://github.com/bitbank2/JPEGDEC.git#ca1e0f2 ; online_image ; This is using the repository until a new release is published to PlatformIO @@ -73,12 +74,11 @@ lib_deps = heman/AsyncMqttClient-esphome@1.0.0 ; mqtt ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base fastled/FastLED@3.9.16 ; fastled_base - mikalhart/TinyGPSPlus@1.1.0 ; gps freekode/TM1651@1.0.1 ; tm1651 glmnet/Dsmr@0.7 ; dsmr rweather/Crypto@0.4.0 ; dsmr dudanov/MideaUART@1.1.9 ; midea - tonia/HeatpumpIR@1.0.35 ; heatpumpir + tonia/HeatpumpIR@1.0.37 ; heatpumpir build_flags = ${common.build_flags} -DUSE_ARDUINO @@ -125,9 +125,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-2/platform-espressif32.zip platform_packages = - pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.1.3/esp32-3.1.3.zip + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip framework = arduino lib_deps = @@ -161,9 +161,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.13/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-2/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.3.2/esp-idf-v5.3.2.zip + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.4.2/esp-idf-v5.4.2.zip framework = espidf lib_deps = @@ -180,13 +180,6 @@ build_unflags = ${common.build_unflags} extra_scripts = post:esphome/components/esp32/post_build.py.script -; This are common settings for the ESP32 using the latest ESP-IDF version. -[common:esp32-idf-5_3] -extends = common:esp32-idf -platform = platformio/espressif32@6.8.0 -platform_packages = - platformio/framework-espidf@~3.50300.0 - ; These are common settings for the RP2040 using Arduino. [common:rp2040-arduino] extends = common:arduino @@ -239,6 +232,7 @@ lib_deps = wjtje/qr-code-generator-library@1.7.0 ; qr_code pavlodn/HaierProtocol@0.9.31 ; haier functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 + https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library lvgl/lvgl@8.4.0 ; lvgl @@ -298,17 +292,6 @@ build_flags = build_unflags = ${common.build_unflags} -[env:esp32-idf-5_3] -extends = common:esp32-idf-5_3 -board = esp32dev -board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf -build_flags = - ${common:esp32-idf.build_flags} - ${flags:runtime.build_flags} - -DUSE_ESP32_VARIANT_ESP32 -build_unflags = - ${common.build_unflags} - [env:esp32-idf-tidy] extends = common:esp32-idf board = esp32dev @@ -353,17 +336,6 @@ build_flags = build_unflags = ${common.build_unflags} -[env:esp32c3-idf-5_3] -extends = common:esp32-idf-5_3 -board = esp32-c3-devkitm-1 -board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c3-idf -build_flags = - ${common:esp32-idf.build_flags} - ${flags:runtime.build_flags} - -DUSE_ESP32_VARIANT_ESP32C3 -build_unflags = - ${common.build_unflags} - [env:esp32c3-idf-tidy] extends = common:esp32-idf board = esp32-c3-devkitm-1 @@ -419,17 +391,6 @@ build_flags = build_unflags = ${common.build_unflags} -[env:esp32s2-idf-5_3] -extends = common:esp32-idf-5_3 -board = esp32-s2-kaluga-1 -board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf -build_flags = - ${common:esp32-idf.build_flags} - ${flags:runtime.build_flags} - -DUSE_ESP32_VARIANT_ESP32S2 -build_unflags = - ${common.build_unflags} - [env:esp32s2-idf-tidy] extends = common:esp32-idf board = esp32-s2-kaluga-1 @@ -474,17 +435,6 @@ build_flags = build_unflags = ${common.build_unflags} -[env:esp32s3-idf-5_3] -extends = common:esp32-idf-5_3 -board = esp32-s3-devkitc-1 -board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s3-idf -build_flags = - ${common:esp32-idf.build_flags} - ${flags:runtime.build_flags} - -DUSE_ESP32_VARIANT_ESP32S3 -build_unflags = - ${common.build_unflags} - [env:esp32s3-idf-tidy] extends = common:esp32-idf board = esp32-s3-devkitc-1 @@ -565,6 +515,8 @@ build_flags = build_unflags = ${common.build_unflags} +;;;;;;;; Host ;;;;;;;; + [env:host] extends = common platform = platformio/native diff --git a/pyproject.toml b/pyproject.toml index 97b0df9eff..4943c48eb0 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,16 +106,20 @@ expected-line-ending-format = "LF" [tool.ruff] required-version = ">=0.5.0" -target-version = "py310" +target-version = "py311" exclude = ['generated'] [tool.ruff.lint] select = [ - "E", # pycodestyle - "F", # pyflakes/autoflake - "I", # isort - "PL", # pylint - "UP", # pyupgrade + "E", # pycodestyle + "F", # pyflakes/autoflake + "FURB", # refurb + "I", # isort + "PERF", # performance + "PL", # pylint + "SIM", # flake8-simplify + "RET", # flake8-ret + "UP", # pyupgrade ] ignore = [ diff --git a/requirements.txt b/requirements.txt index ea264a8ac4..9793336cf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,18 @@ -async_timeout==5.0.1; python_version <= "3.10" cryptography==45.0.1 voluptuous==0.15.2 PyYAML==6.0.2 paho-mqtt==1.6.1 colorama==0.4.6 icmplib==3.0.4 -tornado==6.5.1 +tornado==6.5.2 tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile -esptool==4.9.0 +esptool==5.0.2 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==34.2.1 +aioesphomeapi==38.2.1 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.14 # dashboard_import diff --git a/requirements_test.txt b/requirements_test.txt index ef1fc4f2d6..9ad4591a04 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ -pylint==3.3.7 +pylint==3.3.8 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.8 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit @@ -8,7 +8,7 @@ pre-commit pytest==8.4.1 pytest-cov==6.2.1 pytest-mock==3.14.1 -pytest-asyncio==1.0.0 -pytest-xdist==3.7.0 +pytest-asyncio==1.1.0 +pytest-xdist==3.8.0 asyncmock==0.4.2 hypothesis==6.92.1 diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 3ae1b195e4..3396e5ad05 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 @@ -62,9 +61,7 @@ def indent_list(text: str, padding: str = " ") -> list[str]: """Indent each line of the given text with the specified padding.""" lines = [] for line in text.splitlines(): - if line == "": - p = "" - elif line.startswith("#ifdef") or line.startswith("#endif"): + if line == "" or line.startswith("#ifdef") or line.startswith("#endif"): p = "" else: p = padding @@ -76,6 +73,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) @@ -90,8 +111,15 @@ def force_str(force: bool) -> str: class TypeInfo(ABC): """Base class for all type information.""" - def __init__(self, field: descriptor.FieldDescriptorProto) -> None: + def __init__( + self, + field: descriptor.FieldDescriptorProto, + needs_decode: bool = True, + needs_encode: bool = True, + ) -> None: self._field = field + self._needs_decode = needs_decode + self._needs_encode = needs_encode @property def default_value(self) -> str: @@ -157,13 +185,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 +194,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 +203,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 +212,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 @@ -218,12 +222,26 @@ class TypeInfo(ABC): encode_func = None + @classmethod + def can_use_dump_field(cls) -> bool: + """Whether this type can use the dump_field helper functions. + + Returns True for simple types that have dump_field overloads. + Complex types like messages and bytes should return False. + """ + return True + + def dump_field_value(self, value: str) -> str: + """Get the value expression to pass to dump_field. + + Most types just pass the value directly, but some (like enums) need a cast. + """ + return value + @property def dump_content(self) -> str: - o = f'out.append(" {self.name}: ");\n' - o += self.dump(f"this->{self.field_name}") + "\n" - o += 'out.append("\\n");\n' - return o + # Default implementation - subclasses can override if they need special handling + return f'dump_field(out, "{self.name}", {self.dump_field_value(f"this->{self.field_name}")});' @abstractmethod def dump(self, name: str) -> str: @@ -257,33 +275,13 @@ class TypeInfo(ABC): Args: name: Field name force: Whether this is for a repeated field - base_method: Base method name (e.g., "add_int32_field") + base_method: Base method name (e.g., "add_int32") 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 + method = f"{base_method}_force" if force else base_method value = value_expr if value_expr else name - return f"ProtoSize::{method}(total_size, {field_id_size}, {value});" - - def _get_fixed_size_calculation( - self, name: str, force: bool, num_bytes: int, zero_check: str - ) -> str: - """Helper for fixed-size field calculations. - - Args: - name: Field name - force: Whether this is for a repeated field - num_bytes: Number of bytes (4 or 8) - zero_check: Expression to check for zero value (e.g., "!= 0.0f") - """ - field_id_size = self.calculate_field_id_size() - # Fixed-size repeated fields are handled differently in RepeatedTypeInfo - # so we should never get force=True here - assert not force, ( - "Fixed-size repeated fields should be handled by RepeatedTypeInfo" - ) - method = f"add_fixed_field<{num_bytes}>" - return f"ProtoSize::{method}(total_size, {field_id_size}, {name} {zero_check});" + return f"size.{method}({field_id_size}, {value});" @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: @@ -313,6 +311,67 @@ 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, + needs_decode: bool = True, + needs_encode: bool = True, +) -> TypeInfo: + """Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options.""" + if field.label == 3: # repeated + # Check if this repeated field has fixed_array_with_length_define option + if ( + fixed_size := get_field_opt(field, pb.fixed_array_with_length_define) + ) is not None: + return FixedArrayWithLengthRepeatedType(field, fixed_size) + # Check if this repeated field has fixed_array_size option + if (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None: + return FixedArrayRepeatedType(field, fixed_size) + # Check if this repeated field has fixed_array_size_define option + if ( + size_define := get_field_opt(field, pb.fixed_array_size_define) + ) is not None: + return FixedArrayRepeatedType(field, size_define) + return RepeatedTypeInfo(field) + + # Check for fixed_array_size option on bytes fields + if ( + field.type == 12 + and (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None + ): + return FixedArrayBytesType(field, fixed_size) + + # Special handling for bytes fields + if field.type == 12: + return BytesType(field, needs_decode, needs_encode) + + # Special handling for string fields + if field.type == 9: + return StringType(field, needs_decode, needs_encode) + + validate_field_type(field.type, field.name) + return TYPE_INFO[field.type](field) + def register_type(name: int): """Decorator to register a type with a name and number.""" @@ -339,7 +398,8 @@ class DoubleType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 8, "!= 0.0") + field_id_size = self.calculate_field_id_size() + return f"size.add_double({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -362,7 +422,8 @@ class FloatType(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 4, "!= 0.0f") + field_id_size = self.calculate_field_id_size() + return f"size.add_float({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -385,7 +446,7 @@ class Int64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_int64_field") + return self._get_simple_size_calculation(name, force, "add_int64") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -405,7 +466,7 @@ class UInt64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_uint64_field") + return self._get_simple_size_calculation(name, force, "add_uint64") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -425,7 +486,7 @@ class Int32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_int32_field") + return self._get_simple_size_calculation(name, force, "add_int32") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -445,7 +506,8 @@ class Fixed64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 8, "!= 0") + field_id_size = self.calculate_field_id_size() + return f"size.add_fixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -468,7 +530,8 @@ class Fixed32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 4, "!= 0") + field_id_size = self.calculate_field_id_size() + return f"size.add_fixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -486,11 +549,10 @@ class BoolType(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f"out.append(YESNO({name}));" - return o + return f"out.append(YESNO({name}));" def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_bool_field") + return self._get_simple_size_calculation(name, force, "add_bool") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 1 # field ID + 1 byte @@ -506,12 +568,118 @@ class StringType(TypeInfo): encode_func = "encode_string" wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2 + @property + def public_content(self) -> list[str]: + content: list[str] = [] + + # Check if no_zero_copy option is set + no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) + + # Add std::string storage if message needs decoding OR if no_zero_copy is set + if self._needs_decode or no_zero_copy: + content.append(f"std::string {self.field_name}{{}};") + + # Only add StringRef if encoding is needed AND no_zero_copy is not set + if self._needs_encode and not no_zero_copy: + content.extend( + [ + # Add StringRef field if message needs encoding + f"StringRef {self.field_name}_ref_{{}};", + # Add setter method if message needs encoding + f"void set_{self.field_name}(const StringRef &ref) {{", + f" this->{self.field_name}_ref_ = ref;", + "}", + ] + ) + return content + + @property + def encode_content(self) -> str: + # Check if no_zero_copy option is set + no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) + + if no_zero_copy: + # Use the std::string directly + return f"buffer.encode_string({self.number}, this->{self.field_name});" + # Use the StringRef + return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" + def dump(self, name): - o = f'out.append("\'").append({name}).append("\'");' + # Check if no_zero_copy option is set + no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) + + # If name is 'it', this is a repeated field element - always use string + if name == "it": + return "append_quoted_string(out, StringRef(it));" + + # If no_zero_copy is set, always use std::string + if no_zero_copy: + return f'out.append("\'").append(this->{self.field_name}).append("\'");' + + # For SOURCE_CLIENT only, always use std::string + if not self._needs_encode: + return f'out.append("\'").append(this->{self.field_name}).append("\'");' + + # For SOURCE_SERVER, always use StringRef + if not self._needs_decode: + return f"append_quoted_string(out, this->{self.field_name}_ref_);" + + # For SOURCE_BOTH, check if StringRef is set (sending) or use string (received) + return ( + f"if (!this->{self.field_name}_ref_.empty()) {{" + f' out.append("\'").append(this->{self.field_name}_ref_.c_str()).append("\'");' + f"}} else {{" + f' out.append("\'").append(this->{self.field_name}).append("\'");' + f"}}" + ) + + @property + def dump_content(self) -> str: + # Check if no_zero_copy option is set + no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) + + # If no_zero_copy is set, always use std::string + if no_zero_copy: + return f'dump_field(out, "{self.name}", this->{self.field_name});' + + # For SOURCE_CLIENT only, use std::string + if not self._needs_encode: + return f'dump_field(out, "{self.name}", this->{self.field_name});' + + # For SOURCE_SERVER, use StringRef with _ref_ suffix + if not self._needs_decode: + return f'dump_field(out, "{self.name}", this->{self.field_name}_ref_);' + + # For SOURCE_BOTH, we need custom logic + o = f'out.append(" {self.name}: ");\n' + o += self.dump(f"this->{self.field_name}") + "\n" + o += 'out.append("\\n");' return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_string_field") + # Check if no_zero_copy option is set + no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False) + + # For SOURCE_CLIENT only messages or no_zero_copy, use the string field directly + if not self._needs_encode or no_zero_copy: + # For no_zero_copy, we need to use .size() on the string + if no_zero_copy and name != "it": + field_id_size = self.calculate_field_id_size() + return ( + f"size.add_length({field_id_size}, this->{self.field_name}.size());" + ) + return self._get_simple_size_calculation(name, force, "add_length") + + # Check if this is being called from a repeated field context + # In that case, 'name' will be 'it' and we need to use the repeated version + if name == "it": + # For repeated fields, we need to use add_length_force which includes field ID + field_id_size = self.calculate_field_id_size() + return f"size.add_length_force({field_id_size}, it.size());" + + # For messages that need encoding, use the StringRef size + field_id_size = self.calculate_field_id_size() + return f"size.add_length({field_id_size}, this->{self.field_name}_ref_.size());" def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string @@ -519,6 +687,10 @@ class StringType(TypeInfo): @register_type(11) class MessageType(TypeInfo): + @classmethod + def can_use_dump_field(cls) -> bool: + return False + @property def cpp_type(self) -> str: return self._field.type_name[1:] @@ -549,22 +721,24 @@ class MessageType(TypeInfo): @property def decode_length_content(self) -> str: # Custom decode that doesn't use templates - return dedent( - f"""\ - case {self.number}: {{ - value.decode_to_message(this->{self.field_name}); - return true; - }}""" - ) + return f"case {self.number}: value.decode_to_message(this->{self.field_name}); break;" def dump(self, name: str) -> str: - o = f"{name}.dump_to(out);" + return f"{name}.dump_to(out);" + + @property + def dump_content(self) -> str: + o = f'out.append(" {self.name}: ");\n' + o += f"this->{self.field_name}.dump_to(out);\n" + o += 'out.append("\\n");' return o def get_size_calculation(self, name: str, force: bool = False) -> str: return self._get_simple_size_calculation(name, force, "add_message_object") def get_estimated_size(self) -> int: + # For message types, we can't easily estimate the submessage size without + # access to the actual message definition. This is just a rough estimate. return ( self.calculate_field_id_size() + 16 ) # field ID + 16 bytes estimated submessage @@ -572,29 +746,163 @@ class MessageType(TypeInfo): @register_type(12) class BytesType(TypeInfo): + @classmethod + def can_use_dump_field(cls) -> bool: + return False + cpp_type = "std::string" default_value = "" reference_type = "std::string &" const_reference_type = "const std::string &" - decode_length = "value.as_string()" encode_func = "encode_bytes" + decode_length = "value.as_string()" wire_type = WireType.LENGTH_DELIMITED # Uses wire type 2 + @property + def public_content(self) -> list[str]: + content: list[str] = [] + # Add std::string storage if message needs decoding + if self._needs_decode: + content.append(f"std::string {self.field_name}{{}};") + + if self._needs_encode: + content.extend( + [ + # Add pointer/length fields if message needs encoding + f"const uint8_t* {self.field_name}_ptr_{{nullptr}};", + f"size_t {self.field_name}_len_{{0}};", + # Add setter method if message needs encoding + f"void set_{self.field_name}(const uint8_t* data, size_t len) {{", + f" this->{self.field_name}_ptr_ = data;", + f" this->{self.field_name}_len_ = len;", + "}", + ] + ) + return content + @property def encode_content(self) -> str: - return f"buffer.encode_bytes({self.number}, reinterpret_cast(this->{self.field_name}.data()), this->{self.field_name}.size());" + return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" def dump(self, name: str) -> str: - o = f"out.append(format_hex_pretty({name}));" + ptr_dump = f"format_hex_pretty(this->{self.field_name}_ptr_, this->{self.field_name}_len_)" + str_dump = f"format_hex_pretty(reinterpret_cast(this->{self.field_name}.data()), this->{self.field_name}.size())" + + # For SOURCE_CLIENT only, always use std::string + if not self._needs_encode: + return f"out.append({str_dump});" + + # For SOURCE_SERVER, always use pointer/length + if not self._needs_decode: + return f"out.append({ptr_dump});" + + # For SOURCE_BOTH, check if pointer is set (sending) or use string (received) + return ( + f"if (this->{self.field_name}_ptr_ != nullptr) {{\n" + f" out.append({ptr_dump});\n" + f" }} else {{\n" + f" out.append({str_dump});\n" + f" }}" + ) + + @property + def dump_content(self) -> str: + o = f'out.append(" {self.name}: ");\n' + o += self.dump(f"this->{self.field_name}") + "\n" + o += 'out.append("\\n");' return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_string_field") + return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);" 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.""" + + @classmethod + def can_use_dump_field(cls) -> bool: + return False + + 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: + return f"out.append(format_hex_pretty({name}, {name}_len));" + + @property + def dump_content(self) -> str: + o = f'out.append(" {self.name}: ");\n' + o += f"out.append(format_hex_pretty(this->{self.field_name}, this->{self.field_name}_len));\n" + o += 'out.append("\\n");' + 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 (no zero check) + return f"size.add_length_force({field_id_size}, {length_field});" + # For non-repeated fields, add_length already checks for zero + return f"size.add_length({field_id_size}, {length_field});" + + 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" @@ -609,7 +917,7 @@ class UInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_uint32_field") + return self._get_simple_size_calculation(name, force, "add_uint32") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -637,12 +945,15 @@ class EnumType(TypeInfo): 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 + return f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));" + + def dump_field_value(self, value: str) -> str: + # Enums need explicit cast for the template + return f"static_cast<{self.cpp_type}>({value})" def get_size_calculation(self, name: str, force: bool = False) -> str: return self._get_simple_size_calculation( - name, force, "add_enum_field", f"static_cast({name})" + name, force, "add_uint32", f"static_cast({name})" ) def get_estimated_size(self) -> int: @@ -663,7 +974,8 @@ class SFixed32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 4, "!= 0") + field_id_size = self.calculate_field_id_size() + return f"size.add_sfixed32({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 4 @@ -686,7 +998,8 @@ class SFixed64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_fixed_size_calculation(name, force, 8, "!= 0") + field_id_size = self.calculate_field_id_size() + return f"size.add_sfixed64({field_id_size}, {name});" def get_fixed_size_bytes(self) -> int: return 8 @@ -709,7 +1022,7 @@ class SInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_sint32_field") + return self._get_simple_size_calculation(name, force, "add_sint32") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint @@ -729,19 +1042,288 @@ class SInt64Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: - return self._get_simple_size_calculation(name, force, "add_sint64_field") + return self._get_simple_size_calculation(name, force, "add_sint64") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint +def _generate_array_dump_content( + ti, field_name: str, name: str, is_bool: bool = False +) -> str: + """Generate dump content for array types (repeated or fixed array). + + Shared helper to avoid code duplication between RepeatedTypeInfo and FixedArrayRepeatedType. + """ + o = f"for (const auto {'' if is_bool else '&'}it : {field_name}) {{\n" + # Check if underlying type can use dump_field + if ti.can_use_dump_field(): + # For types that have dump_field overloads, use them with extra indent + o += f' dump_field(out, "{name}", {ti.dump_field_value("it")}, 4);\n' + else: + # For complex types (messages, bytes), use the old pattern + o += f' out.append(" {name}: ");\n' + o += indent(ti.dump("it")) + "\n" + o += ' out.append("\\n");\n' + o += "}" + return o + + +class FixedArrayRepeatedType(TypeInfo): + """Special type for fixed-size repeated fields using std::array. + + Fixed arrays are only supported for encoding (SOURCE_SERVER) since we cannot + control how many items we receive when decoding. + """ + + def __init__(self, field: descriptor.FieldDescriptorProto, size: int | str) -> None: + super().__init__(field) + self.array_size = size + self.is_define = isinstance(size, str) + # Check if we should skip encoding when all elements are zero + # Use getattr to handle older versions of api_options_pb2 + self.skip_zero = get_field_opt( + field, getattr(pb, "fixed_array_skip_zero", None), False + ) + # Create the element type info + validate_field_type(field.type, field.name) + self._ti: TypeInfo = TYPE_INFO[field.type](field) + + def _encode_element(self, element: str) -> str: + """Helper to generate encode statement for a single element.""" + if isinstance(self._ti, EnumType): + return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" + return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" + + @property + def cpp_type(self) -> str: + return f"std::array<{self._ti.cpp_type}, {self.array_size}>" + + @property + def reference_type(self) -> str: + return f"{self.cpp_type} &" + + @property + def const_reference_type(self) -> str: + return f"const {self.cpp_type} &" + + @property + def wire_type(self) -> WireType: + """Get the wire type for this fixed array field.""" + return self._ti.wire_type + + @property + def public_content(self) -> list[str]: + # Just the array member, no index needed since we don't decode + return [f"{self.cpp_type} {self.field_name}{{}};"] + + # No decode methods needed - fixed arrays don't support decoding + # The base class TypeInfo already returns None for all decode properties + + @property + def encode_content(self) -> str: + # If skip_zero is enabled, wrap encoding in a zero check + if self.skip_zero: + if self.is_define: + # When using a define, we need to use a loop-based approach + o = f"for (const auto &it : this->{self.field_name}) {{\n" + o += " if (it != 0) {\n" + o += f" {self._encode_element('it')}\n" + o += " }\n" + o += "}" + return o + # Build the condition to check if at least one element is non-zero + non_zero_checks = " || ".join( + [f"this->{self.field_name}[{i}] != 0" for i in range(self.array_size)] + ) + encode_lines = [ + f" {self._encode_element(f'this->{self.field_name}[{i}]')}" + for i in range(self.array_size) + ] + return f"if ({non_zero_checks}) {{\n" + "\n".join(encode_lines) + "\n}" + + # When using a define, always use loop-based approach + if self.is_define: + o = f"for (const auto &it : this->{self.field_name}) {{\n" + o += f" {self._encode_element('it')}\n" + o += "}" + return o + + # Unroll small arrays for efficiency + if self.array_size == 1: + return self._encode_element(f"this->{self.field_name}[0]") + if self.array_size == 2: + return ( + self._encode_element(f"this->{self.field_name}[0]") + + "\n " + + self._encode_element(f"this->{self.field_name}[1]") + ) + + # Use loops for larger arrays + o = f"for (const auto &it : this->{self.field_name}) {{\n" + o += f" {self._encode_element('it')}\n" + o += "}" + return o + + @property + def dump_content(self) -> str: + return _generate_array_dump_content( + self._ti, f"this->{self.field_name}", self.name, is_bool=False + ) + + def dump(self, name: str) -> str: + # This is used when dumping the array itself (not its elements) + # Since dump_content handles the iteration, this is not used directly + return "" + + def get_size_calculation(self, name: str, force: bool = False) -> str: + # If skip_zero is enabled, wrap size calculation in a zero check + if self.skip_zero: + if self.is_define: + # When using a define, we need to use a loop-based approach + o = f"for (const auto &it : {name}) {{\n" + o += " if (it != 0) {\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" + o += " }\n" + o += "}" + return o + # Build the condition to check if at least one element is non-zero + non_zero_checks = " || ".join( + [f"{name}[{i}] != 0" for i in range(self.array_size)] + ) + size_lines = [ + f" {self._ti.get_size_calculation(f'{name}[{i}]', True)}" + for i in range(self.array_size) + ] + return f"if ({non_zero_checks}) {{\n" + "\n".join(size_lines) + "\n}" + + # When using a define, always use loop-based approach + if self.is_define: + o = f"for (const auto &it : {name}) {{\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" + o += "}" + return o + + # For fixed arrays, we always encode all elements + + # Special case for single-element arrays - no loop needed + if self.array_size == 1: + return self._ti.get_size_calculation(f"{name}[0]", True) + + # Special case for 2-element arrays - unroll the calculation + if self.array_size == 2: + return ( + self._ti.get_size_calculation(f"{name}[0]", True) + + "\n " + + self._ti.get_size_calculation(f"{name}[1]", True) + ) + + # Use loops for larger arrays + o = f"for (const auto &it : {name}) {{\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" + o += "}" + return o + + def get_estimated_size(self) -> int: + # For fixed arrays, estimate underlying type size * array size + underlying_size = self._ti.get_estimated_size() + if self.is_define: + # When using a define, we don't know the actual size so just guess 3 + # This is only used for documentation and never actually used since + # fixed arrays are only for SOURCE_SERVER (encode-only) messages + return underlying_size * 3 + return underlying_size * self.array_size + + +class FixedArrayWithLengthRepeatedType(FixedArrayRepeatedType): + """Special type for fixed-size repeated fields with variable length tracking. + + Similar to FixedArrayRepeatedType but generates an additional length field + to track how many elements are actually in use. Only encodes/sends elements + up to the current length. + + Fixed arrays with length are only supported for encoding (SOURCE_SERVER) since + we cannot control how many items we receive when decoding. + """ + + @property + def public_content(self) -> list[str]: + # Return both the array and the length field + return [ + f"{self.cpp_type} {self.field_name}{{}};", + f"uint16_t {self.field_name}_len{{0}};", + ] + + @property + def encode_content(self) -> str: + # Always use a loop up to the current length + o = f"for (uint16_t i = 0; i < this->{self.field_name}_len; i++) {{\n" + o += f" {self._encode_element(f'this->{self.field_name}[i]')}\n" + o += "}" + return o + + @property + def dump_content(self) -> str: + # Dump only the active elements + o = f"for (uint16_t i = 0; i < this->{self.field_name}_len; i++) {{\n" + # Check if underlying type can use dump_field + if self._ti.can_use_dump_field(): + o += f' dump_field(out, "{self.name}", {self._ti.dump_field_value(f"this->{self.field_name}[i]")}, 4);\n' + else: + o += f' out.append(" {self.name}: ");\n' + o += indent(self._ti.dump(f"this->{self.field_name}[i]")) + "\n" + o += ' out.append("\\n");\n' + o += "}" + return o + + def get_size_calculation(self, name: str, force: bool = False) -> str: + # Calculate size only for active elements + o = f"for (uint16_t i = 0; i < {name}_len; i++) {{\n" + o += f" {self._ti.get_size_calculation(f'{name}[i]', True)}\n" + o += "}" + return o + + def get_estimated_size(self) -> int: + # For fixed arrays with length, estimate based on typical usage + # Assume on average half the array is used + underlying_size = self._ti.get_estimated_size() + if self.is_define: + # When using a define, estimate 8 elements as typical + return underlying_size * 8 + return underlying_size * ( + self.array_size // 2 if self.array_size > 2 else self.array_size + ) + + class RepeatedTypeInfo(TypeInfo): def __init__(self, field: descriptor.FieldDescriptorProto) -> None: super().__init__(field) + # Check if this is a pointer field by looking for container_pointer option + self._container_type = get_field_opt(field, pb.container_pointer, "") + self._use_pointer = bool(self._container_type) + + # 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 def cpp_type(self) -> str: + if self._use_pointer and self._container_type: + # For pointer fields, use the specified container type + # If the container type already includes the element type (e.g., std::set) + # use it as-is, otherwise append the element type + if "<" in self._container_type and ">" in self._container_type: + return f"const {self._container_type}*" + return f"const {self._container_type}<{self._ti.cpp_type}>*" return f"std::vector<{self._ti.cpp_type}>" @property @@ -762,64 +1344,53 @@ class RepeatedTypeInfo(TypeInfo): @property def decode_varint_content(self) -> str: + # Pointer fields don't support decoding + if self._use_pointer: + return None 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: + # Pointer fields don't support decoding + if self._use_pointer: + return None content = self._ti.decode_length if content is None and isinstance(self._ti, MessageType): # Special handling for non-template message decoding - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name}.emplace_back(); - value.decode_to_message(this->{self.field_name}.back()); - return true; - }}""" - ) + return f"case {self.number}: this->{self.field_name}.emplace_back(); value.decode_to_message(this->{self.field_name}.back()); break;" if content is None: return None - return dedent( - f"""\ - case {self.number}: {{ - this->{self.field_name}.push_back({content}); - return true; - }}""" + return ( + f"case {self.number}: this->{self.field_name}.push_back({content}); break;" ) @property def decode_32bit_content(self) -> str: + # Pointer fields don't support decoding + if self._use_pointer: + return None 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 def decode_64bit_content(self) -> str: + # Pointer fields don't support decoding + if self._use_pointer: + return None 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 @@ -829,6 +1400,15 @@ class RepeatedTypeInfo(TypeInfo): @property def encode_content(self) -> str: + if self._use_pointer: + # For pointer fields, just dereference (pointer should never be null in our use case) + o = f"for (const auto &it : *this->{self.field_name}) {{\n" + if isinstance(self._ti, EnumType): + o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" + else: + o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" + o += "}" + return o o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" if isinstance(self._ti, EnumType): o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" @@ -839,12 +1419,14 @@ class RepeatedTypeInfo(TypeInfo): @property def dump_content(self) -> str: - o = f"for (const auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" - o += f' out.append(" {self.name}: ");\n' - o += indent(self._ti.dump("it")) + "\n" - o += ' out.append("\\n");\n' - o += "}\n" - return o + if self._use_pointer: + # For pointer fields, dereference and use the existing helper + return _generate_array_dump_content( + self._ti, f"*this->{self.field_name}", self.name, is_bool=False + ) + return _generate_array_dump_content( + self._ti, f"this->{self.field_name}", self.name, is_bool=self._ti_is_bool + ) def dump(self, _: str): pass @@ -852,28 +1434,34 @@ class RepeatedTypeInfo(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: # For repeated fields, we always need to pass force=True to the underlying type's calculation # This is because the encode method always sets force=true for repeated fields + + # Handle message types separately as they use a dedicated helper if isinstance(self._ti, MessageType): - # For repeated messages, use the dedicated helper that handles iteration internally field_id_size = self._ti.calculate_field_id_size() - o = f"ProtoSize::add_repeated_message(total_size, {field_id_size}, {name});" - return o + container = f"*{name}" if self._use_pointer else name + return f"size.add_repeated_message({field_id_size}, {container});" - # For other repeated types, use the underlying type's size calculation with force=True - o = f"if (!{name}.empty()) {{\n" + # For non-message types, generate size calculation with iteration + container_ref = f"*{name}" if self._use_pointer else name + empty_check = f"{name}->empty()" if self._use_pointer else f"{name}.empty()" - # Check if this is a fixed-size type by seeing if it has a fixed byte count + o = f"if (!{empty_check}) {{\n" + + # Check if this is a fixed-size type 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 + # Fixed types have constant size per element 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" + size_expr = f"{name}->size()" if self._use_pointer else f"{name}.size()" + o += f" size.add_precalculated_size({size_expr} * {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" + auto_ref = "" if self._ti_is_bool else "&" + o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" o += f" {self._ti.get_size_calculation('it', True)}\n" o += " }\n" + o += "}" return o @@ -889,14 +1477,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[ @@ -905,6 +1494,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] = { @@ -912,18 +1502,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: @@ -949,9 +1556,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 @@ -983,7 +1597,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]: @@ -1025,10 +1687,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() @@ -1038,7 +1701,8 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int: def build_message_type( desc: descriptor.DescriptorProto, - base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]] = None, + base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]], + message_source_map: dict[str, int], ) -> tuple[str, str, str]: public_content: list[str] = [] protected_content: list[str] = [] @@ -1060,7 +1724,7 @@ def build_message_type( message_id: int | None = get_opt(desc, pb.id) # Get source direction to determine if we need decode/encode methods - source: int = get_opt(desc, pb.source, SOURCE_BOTH) + source = message_source_map[desc.name] needs_decode = source in (SOURCE_BOTH, SOURCE_CLIENT) needs_encode = source in (SOURCE_BOTH, SOURCE_SERVER) @@ -1095,72 +1759,139 @@ 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 + + # Validate that fixed_array_size is only used in encode-only messages + if ( + needs_decode + and field.label == 3 + and get_field_opt(field, pb.fixed_array_size) is not None + ): + raise ValueError( + f"Message '{desc.name}' uses fixed_array_size on field '{field.name}' " + f"but has source={SOURCE_NAMES[source]}. " + f"Fixed arrays are only supported for SOURCE_SERVER (encode-only) messages " + f"since we cannot trust or control the number of items received from clients." + ) + + # Validate that fixed_array_with_length_define is only used in encode-only messages + if ( + needs_decode + and field.label == 3 + and get_field_opt(field, pb.fixed_array_with_length_define) is not None + ): + raise ValueError( + f"Message '{desc.name}' uses fixed_array_with_length_define on field '{field.name}' " + f"but has source={SOURCE_NAMES[source]}. " + f"Fixed arrays with length are only supported for SOURCE_SERVER (encode-only) messages " + f"since we cannot trust or control the number of items received from clients." + ) + + ti = create_field_type_info(field, needs_decode, needs_encode) # Skip field declarations for fields that are in the base class # but include their encode/decode logic if field.name not in common_field_names: - protected_content.extend(ti.protected_content) - public_content.extend(ti.public_content) + # Check for field_ifdef option + field_ifdef = None + if field.options.HasExtension(pb.field_ifdef): + field_ifdef = field.options.Extensions[pb.field_ifdef] + + if ti.protected_content: + protected_content.extend( + wrap_with_ifdef(ti.protected_content, field_ifdef) + ) + if ti.public_content: + public_content.extend(wrap_with_ifdef(ti.public_content, field_ifdef)) # Only collect encode logic if this message needs it if needs_encode: - encode.append(ti.encode_content) - size_calc.append(ti.get_size_calculation(f"this->{ti.field_name}")) + # Check for field_ifdef option + field_ifdef = None + if field.options.HasExtension(pb.field_ifdef): + field_ifdef = field.options.Extensions[pb.field_ifdef] + + encode.extend(wrap_with_ifdef(ti.encode_content, field_ifdef)) + size_calc.extend( + wrap_with_ifdef( + ti.get_size_calculation(f"this->{ti.field_name}"), field_ifdef + ) + ) # Only collect decode methods if this message needs them if needs_decode: + # Check for field_ifdef option for decode as well + field_ifdef = None + if field.options.HasExtension(pb.field_ifdef): + field_ifdef = field.options.Extensions[pb.field_ifdef] + if ti.decode_varint_content: - decode_varint.append(ti.decode_varint_content) + decode_varint.extend( + wrap_with_ifdef(ti.decode_varint_content, field_ifdef) + ) if ti.decode_length_content: - decode_length.append(ti.decode_length_content) + decode_length.extend( + wrap_with_ifdef(ti.decode_length_content, field_ifdef) + ) if ti.decode_32bit_content: - decode_32bit.append(ti.decode_32bit_content) + decode_32bit.extend( + wrap_with_ifdef(ti.decode_32bit_content, field_ifdef) + ) if ti.decode_64bit_content: - decode_64bit.append(ti.decode_64bit_content) + decode_64bit.extend( + wrap_with_ifdef(ti.decode_64bit_content, field_ifdef) + ) if ti.dump_content: - dump.append(ti.dump_content) + # Check for field_ifdef option for dump as well + field_ifdef = None + if field.options.HasExtension(pb.field_ifdef): + field_ifdef = field.options.Extensions[pb.field_ifdef] + + dump.extend(wrap_with_ifdef(ti.dump_content, field_ifdef)) cpp = "" if decode_varint: - decode_varint.append("default:\n return false;") o = f"bool {desc.name}::decode_varint(uint32_t field_id, ProtoVarInt value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_varint), " ") + "\n" + o += " default: return false;\n" o += " }\n" + o += " return true;\n" o += "}\n" cpp += o prot = "bool decode_varint(uint32_t field_id, ProtoVarInt value) override;" protected_content.insert(0, prot) if decode_length: - decode_length.append("default:\n return false;") o = f"bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_length), " ") + "\n" + o += " default: return false;\n" o += " }\n" + o += " return true;\n" o += "}\n" cpp += o prot = "bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;" protected_content.insert(0, prot) if decode_32bit: - decode_32bit.append("default:\n return false;") o = f"bool {desc.name}::decode_32bit(uint32_t field_id, Proto32Bit value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_32bit), " ") + "\n" + o += " default: return false;\n" o += " }\n" + o += " return true;\n" o += "}\n" cpp += o prot = "bool decode_32bit(uint32_t field_id, Proto32Bit value) override;" protected_content.insert(0, prot) if decode_64bit: - decode_64bit.append("default:\n return false;") o = f"bool {desc.name}::decode_64bit(uint32_t field_id, Proto64Bit value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_64bit), " ") + "\n" + o += " default: return false;\n" o += " }\n" + o += " return true;\n" o += "}\n" cpp += o prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;" @@ -1170,11 +1901,11 @@ def build_message_type( 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]} " + o += f" {encode[0]} }}\n" else: o += "\n" o += indent("\n".join(encode)) + "\n" - o += "}\n" + o += "}\n" cpp += o prot = "void encode(ProtoWriteBuffer buffer) const override;" public_content.append(prot) @@ -1182,17 +1913,17 @@ def build_message_type( # 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 {{" + o = f"void {desc.name}::calculate_size(ProtoSize &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: - o += f" {size_calc[0]} " + o += f" {size_calc[0]} }}\n" else: # For multiple fields o += "\n" o += indent("\n".join(size_calc)) + "\n" - o += "}\n" + o += "}\n" cpp += o - prot = "void calculate_size(uint32_t &total_size) const override;" + prot = "void calculate_size(ProtoSize &size) const override;" public_content.append(prot) # If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used @@ -1209,10 +1940,8 @@ def build_message_type( dump_impl += f" {dump[0]} " else: dump_impl += "\n" - dump_impl += " __attribute__((unused)) char buffer[64];\n" - dump_impl += f' out.append("{desc.name} {{\\n");\n' + dump_impl += f' MessageDumpHelper helper(out, "{desc.name}");\n' dump_impl += indent("\n".join(dump)) + "\n" - dump_impl += ' out.append("}");\n' else: o2 = f'out.append("{desc.name} {{}}");' if len(dump_impl) + len(o2) + 3 < 120: @@ -1225,7 +1954,14 @@ 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" + # Check if message has any non-deprecated fields + has_fields = any(not field.options.deprecated for field in desc.field) + # Determine inheritance based on whether the message needs decoding and has fields + if needs_decode and has_fields: + base_class = "ProtoDecodableMessage" + else: + base_class = "ProtoMessage" + out = f"class {desc.name} : public {base_class} {{\n" out += " public:\n" out += indent("\n".join(public_content)) + "\n" out += "\n" @@ -1245,6 +1981,12 @@ SOURCE_BOTH = 0 SOURCE_SERVER = 1 SOURCE_CLIENT = 2 +SOURCE_NAMES = { + SOURCE_BOTH: "SOURCE_BOTH", + SOURCE_SERVER: "SOURCE_SERVER", + SOURCE_CLIENT: "SOURCE_CLIENT", +} + RECEIVE_CASES: dict[int, tuple[str, str | None]] = {} ifdefs: dict[str, str] = {} @@ -1261,6 +2003,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): @@ -1291,8 +2044,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 @@ -1303,6 +2058,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 @@ -1323,28 +2081,65 @@ 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 = [] protected_content = [] + # Determine if any message using this base class needs decoding/encoding + needs_decode = any( + message_source_map.get(msg.name, SOURCE_BOTH) in (SOURCE_BOTH, SOURCE_CLIENT) + for msg in messages + ) + needs_encode = any( + message_source_map.get(msg.name, SOURCE_BOTH) in (SOURCE_BOTH, SOURCE_SERVER) + for msg in messages + ) + # 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, needs_decode, needs_encode) + + # 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)) # Build header - out = f"class {base_class_name} : public ProtoMessage {{\n" + parent_class = "ProtoDecodableMessage" if needs_decode else "ProtoMessage" + out = f"class {base_class_name} : public {parent_class} {{\n" out += " public:\n" # Add destructor with override @@ -1370,6 +2165,7 @@ def build_base_class( def generate_base_classes( base_class_groups: dict[str, list[descriptor.DescriptorProto]], + message_source_map: dict[str, int], ) -> tuple[str, str, str]: """Generate all base classes.""" all_headers = [] @@ -1382,7 +2178,9 @@ def generate_base_classes( if common_fields: # Generate base class - header, cpp, dump_cpp = build_base_class(base_class_name, common_fields) + header, cpp, dump_cpp = build_base_class( + base_class_name, common_fields, messages, message_source_map + ) all_headers.append(header) all_cpp.append(cpp) all_dump_cpp.append(dump_cpp) @@ -1392,14 +2190,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) @@ -1423,15 +2226,22 @@ def build_service_message_type( hout += f"virtual void {func}(const {mt.name} &value){{}};\n" case = "" case += f"{mt.name} msg;\n" - case += "msg.decode(msg_data, msg_size);\n" + # Check if this message has any fields (excluding deprecated ones) + has_fields = any(not field.options.deprecated for field in mt.field) + if has_fields: + # Normal case: decode the message + case += "msg.decode(msg_data, msg_size);\n" + else: + # Empty message optimization: skip decode since there are no fields + case += "// Empty message: no decode needed\n" if log: case += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' case += "#endif\n" case += f"this->{func}(msg);\n" case += "break;" - # Store the ifdef with the case for later use - RECEIVE_CASES[id_] = (case, ifdef) + # Store the message name and ifdef with the case for later use + RECEIVE_CASES[id_] = (case, ifdef, mt.name) # Only close ifdef if we opened it if ifdef is not None: @@ -1452,16 +2262,20 @@ def main() -> None: d = descriptor.FileDescriptorSet.FromString(proto_content) file = d.file[0] + content = FILE_HEADER content += """\ #pragma once #include "esphome/core/defines.h" +#include "esphome/core/string_ref.h" #include "proto.h" +#include "api_pb2_includes.h" +""" -namespace esphome { -namespace api { + content += """ +namespace esphome::api { """ @@ -1470,9 +2284,9 @@ namespace api { #include "api_pb2.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" + #include -namespace esphome { -namespace api { +namespace esphome::api { """ @@ -1486,20 +2300,111 @@ namespace api { #ifdef HAS_PROTO_MESSAGE_DUMP -namespace esphome { -namespace api { +namespace esphome::api { + +// Helper function to append a quoted string, handling empty StringRef +static inline void append_quoted_string(std::string &out, const StringRef &ref) { + out.append("'"); + if (!ref.empty()) { + out.append(ref.c_str()); + } + out.append("'"); +} + +// Common helpers for dump_field functions +static inline void append_field_prefix(std::string &out, const char *field_name, int indent) { + out.append(indent, ' ').append(field_name).append(": "); +} + +static inline void append_with_newline(std::string &out, const char *str) { + out.append(str); + out.append("\\n"); +} + +// RAII helper for message dump formatting +class MessageDumpHelper { + public: + MessageDumpHelper(std::string &out, const char *message_name) : out_(out) { + out_.append(message_name); + out_.append(" {\\n"); + } + ~MessageDumpHelper() { out_.append(" }"); } + + private: + std::string &out_; +}; + +// Helper functions to reduce code duplication in dump methods +static void dump_field(std::string &out, const char *field_name, int32_t value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%" PRId32, value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, uint32_t value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%" PRIu32, value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, float value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%g", value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { + char buffer[64]; + append_field_prefix(out, field_name, indent); + snprintf(buffer, 64, "%llu", value); + append_with_newline(out, buffer); +} + +static void dump_field(std::string &out, const char *field_name, bool value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append(YESNO(value)); + out.append("\\n"); +} + +static void dump_field(std::string &out, const char *field_name, const std::string &value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append("'").append(value).append("'"); + out.append("\\n"); +} + +static void dump_field(std::string &out, const char *field_name, StringRef value, int indent = 2) { + append_field_prefix(out, field_name, indent); + append_quoted_string(out, value); + out.append("\\n"); +} + +template +static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { + append_field_prefix(out, field_name, indent); + out.append(proto_enum_to_string(value)); + out.append("\\n"); +} """ 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) @@ -1538,7 +2443,9 @@ namespace api { # Generate base classes if base_class_fields: - base_headers, base_cpp, base_dump_cpp = generate_base_classes(base_class_groups) + base_headers, base_cpp, base_dump_cpp = generate_base_classes( + base_class_groups, message_source_map + ) content += base_headers cpp += base_cpp dump_cpp += base_dump_cpp @@ -1548,7 +2455,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 @@ -1577,19 +2492,16 @@ namespace api { content += """\ -} // namespace api -} // namespace esphome +} // namespace esphome::api """ cpp += """\ -} // namespace api -} // namespace esphome +} // namespace esphome::api """ dump_cpp += """\ -} // namespace api -} // namespace esphome +} // namespace esphome::api #endif // HAS_PROTO_MESSAGE_DUMP """ @@ -1611,8 +2523,7 @@ namespace api { #include "api_pb2.h" -namespace esphome { -namespace api { +namespace esphome::api { """ @@ -1621,8 +2532,7 @@ namespace api { #include "api_pb2_service.h" #include "esphome/core/log.h" -namespace esphome { -namespace api { +namespace esphome::api { static const char *const TAG = "api.service"; @@ -1640,13 +2550,12 @@ static const char *const TAG = "api.service"; hpp += " public:\n" hpp += "#endif\n\n" - # Add generic send_message method - hpp += " template\n" - hpp += " bool send_message(const T &msg) {\n" + # Add non-template send_message method + hpp += " bool send_message(const ProtoMessage &msg, uint8_t message_type) {\n" hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" hpp += " this->log_send_message_(msg.message_name(), msg.dump());\n" hpp += "#endif\n" - hpp += " return this->send_message_(msg, T::MESSAGE_TYPE);\n" + hpp += " return this->send_message_(msg, message_type);\n" hpp += " }\n\n" # Add logging helper method implementation to cpp @@ -1657,7 +2566,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 @@ -1670,10 +2579,11 @@ static const char *const TAG = "api.service"; hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" out += " switch (msg_type) {\n" - for i, (case, ifdef) in cases: + for i, (case, ifdef, message_name) in cases: if ifdef is not None: out += f"#ifdef {ifdef}\n" - c = f" case {i}: {{\n" + + c = f" case {message_name}::MESSAGE_TYPE: {{\n" c += indent(case, " ") + "\n" c += " }" out += c + "\n" @@ -1705,7 +2615,7 @@ static const char *const TAG = "api.service"; needs_conn = get_opt(m, pb.needs_setup_connection, True) needs_auth = get_opt(m, pb.needs_authentication, True) - ifdef = message_ifdef_map.get(inp, ifdefs.get(inp, None)) + ifdef = message_ifdef_map.get(inp, ifdefs.get(inp)) if ifdef is not None: hpp += f"#ifdef {ifdef}\n" @@ -1713,7 +2623,13 @@ static const char *const TAG = "api.service"; cpp += f"#ifdef {ifdef}\n" hpp_protected += f" void {on_func}(const {inp} &msg) override;\n" - hpp += f" virtual {ret} {func}(const {inp} &msg) = 0;\n" + + # For non-void methods, generate a send_ method instead of return-by-value + if is_void: + hpp += f" virtual void {func}(const {inp} &msg) = 0;\n" + else: + hpp += f" virtual bool send_{func}_response(const {inp} &msg) = 0;\n" + cpp += f"void {class_name}::{on_func}(const {inp} &msg) {{\n" # Start with authentication/connection check if needed @@ -1724,28 +2640,23 @@ static const char *const TAG = "api.service"; else: check_func = "this->check_connection_setup_()" - body = f"if ({check_func}) {{\n" - - # Add the actual handler code, indented - handler_body = "" if is_void: - handler_body = f"this->{func}(msg);\n" + # For void methods, just wrap with auth check + body = f"if ({check_func}) {{\n" + body += f" this->{func}(msg);\n" + body += "}\n" else: - handler_body = f"{ret} ret = this->{func}(msg);\n" - handler_body += "if (!this->send_message(ret)) {\n" - handler_body += " this->on_fatal_error();\n" - handler_body += "}\n" - - body += indent(handler_body) + "\n" - body += "}\n" + # For non-void methods, combine auth check and send response check + body = f"if ({check_func} && !this->send_{func}_response(msg)) {{\n" + body += " this->on_fatal_error();\n" + body += "}\n" else: # No auth check needed, just call the handler body = "" if is_void: 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_{func}_response(msg)) {{\n" body += " this->on_fatal_error();\n" body += "}\n" @@ -1762,13 +2673,11 @@ static const char *const TAG = "api.service"; hpp += """\ -} // namespace api -} // namespace esphome +} // namespace esphome::api """ cpp += """\ -} // namespace api -} // namespace esphome +} // namespace esphome::api """ with open(root / "api_pb2_service.h", "w", encoding="utf-8") as f: 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/build_language_schema.py b/script/build_language_schema.py index 4473ec1b5a..ff6e898902 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -71,11 +71,13 @@ def get_component_names(): skip_components = [] for d in os.listdir(CORE_COMPONENTS_PATH): - if not d.startswith("__") and os.path.isdir( - os.path.join(CORE_COMPONENTS_PATH, d) + if ( + not d.startswith("__") + and os.path.isdir(os.path.join(CORE_COMPONENTS_PATH, d)) + and d not in component_names + and d not in skip_components ): - if d not in component_names and d not in skip_components: - component_names.append(d) + component_names.append(d) return sorted(component_names) @@ -139,11 +141,10 @@ def register_module_schemas(key, module, manifest=None): for name, schema in module_schemas(module): register_known_schema(key, name, schema) - if manifest: + if manifest and manifest.multi_conf and S_CONFIG_SCHEMA in output[key][S_SCHEMAS]: # Multi conf should allow list of components # not sure about 2nd part of the if, might be useless config (e.g. as3935) - if manifest.multi_conf and S_CONFIG_SCHEMA in output[key][S_SCHEMAS]: - output[key][S_SCHEMAS][S_CONFIG_SCHEMA]["is_list"] = True + output[key][S_SCHEMAS][S_CONFIG_SCHEMA]["is_list"] = True def register_known_schema(module, name, schema): @@ -230,7 +231,7 @@ def add_module_registries(domain, module): reg_type = attr_name.partition("_")[0].lower() found_registries[repr(attr_obj)] = f"{domain}.{reg_type}" - for name in attr_obj.keys(): + for name in attr_obj: if "." not in name: reg_entry_name = name else: @@ -411,11 +412,16 @@ def add_referenced_recursive(referenced_schemas, config_var, path, eat_schema=Fa s1 = get_str_path_schema(k) p = k.split(".") - if len(p) == 3 and path[0] == f"{p[0]}.{p[1]}": - # special case for schema inside platforms - add_referenced_recursive( - referenced_schemas, s1, [path[0], "schemas", p[2]] - ) + if len(p) == 3: + if path[0] == f"{p[0]}.{p[1]}": + # special case for schema inside platforms + add_referenced_recursive( + referenced_schemas, s1, [path[0], "schemas", p[2]] + ) + else: + add_referenced_recursive( + referenced_schemas, s1, [f"{p[0]}.{p[1]}", "schemas", p[2]] + ) else: add_referenced_recursive( referenced_schemas, s1, [p[0], "schemas", p[1]] @@ -438,8 +444,7 @@ def get_str_path_schema(strPath): if len(parts) > 2: parts[0] += "." + parts[1] parts[1] = parts[2] - s1 = output.get(parts[0], {}).get(S_SCHEMAS, {}).get(parts[1], {}) - return s1 + return output.get(parts[0], {}).get(S_SCHEMAS, {}).get(parts[1], {}) def pop_str_path_schema(strPath): @@ -695,7 +700,7 @@ def is_convertible_schema(schema): if repr(schema) in ejs.registry_schemas: return True if isinstance(schema, dict): - for k in schema.keys(): + for k in schema: if isinstance(k, (cv.Required, cv.Optional)): return True return False @@ -813,7 +818,7 @@ def convert(schema, config_var, path): elif schema_type == "automation": extra_schema = None config_var[S_TYPE] = "trigger" - if automation.AUTOMATION_SCHEMA == ejs.extended_schemas[repr(data)][0]: + if ejs.extended_schemas[repr(data)][0] == automation.AUTOMATION_SCHEMA: extra_schema = ejs.extended_schemas[repr(data)][1] if ( extra_schema is not None and len(extra_schema) > 1 @@ -921,9 +926,8 @@ def convert(schema, config_var, path): config = convert_config(schema_type, path + "/type_" + schema_key) types[schema_key] = config["schema"] - elif DUMP_UNKNOWN: - if S_TYPE not in config_var: - config_var["unknown"] = repr_schema + elif DUMP_UNKNOWN and S_TYPE not in config_var: + config_var["unknown"] = repr_schema if DUMP_PATH: config_var["path"] = path diff --git a/script/ci-custom.py b/script/ci-custom.py index d0b518251f..61081608d5 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -197,7 +197,7 @@ def lint_content_find_check(find, only_first=False, **kwargs): find_ = find(fname, content) errs = [] for line, col in find_all(content, find_): - err = func(fname) + err = func(fname, line, col, content) errs.append((line + 1, col + 1, err)) if only_first: break @@ -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): @@ -261,16 +264,16 @@ def lint_executable_bit(fname): "esphome/dashboard/static/ext-searchbox.js", ], ) -def lint_tabs(fname): +def lint_tabs(fname, line, col, content): return "File contains tab character. Please convert tabs to spaces." @lint_content_find_check("\r", only_first=True) -def lint_newline(fname): +def lint_newline(fname, line, col, content): 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." @@ -497,7 +500,8 @@ def lint_constants_usage(): continue errs.append( f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the " - f"constant to const.py (Uses: {', '.join(uses)})" + f"constant to const.py (Uses: {', '.join(uses)}) in a separate PR. " + "See https://developers.esphome.io/contributing/code/#python" ) return errs @@ -509,7 +513,7 @@ def relative_cpp_search_text(fname, content): @lint_content_find_check(relative_cpp_search_text, include=["esphome/components/*.cpp"]) -def lint_relative_cpp_import(fname): +def lint_relative_cpp_import(fname, line, col, content): return ( "Component contains absolute import - Components must always use " "relative imports.\n" @@ -526,6 +530,20 @@ def relative_py_search_text(fname, content): return f"esphome.components.{integration}" +def convert_path_to_relative(abspath, current): + """Convert an absolute path to a relative import path.""" + if abspath == current: + return "." + absparts = abspath.split(".") + curparts = current.split(".") + uplen = len(curparts) + while absparts and curparts and absparts[0] == curparts[0]: + absparts.pop(0) + curparts.pop(0) + uplen -= 1 + return "." * uplen + ".".join(absparts) + + @lint_content_find_check( relative_py_search_text, include=["esphome/components/*.py"], @@ -534,14 +552,19 @@ def relative_py_search_text(fname, content): "esphome/components/web_server/__init__.py", ], ) -def lint_relative_py_import(fname): +def lint_relative_py_import(fname, line, col, content): + import_line = content.splitlines()[line] + abspath = import_line[col:].split(" ")[0] + current = fname.removesuffix(".py").replace(os.path.sep, ".") + replacement = convert_path_to_relative(abspath, current) + newline = import_line.replace(abspath, replacement) return ( "Component contains absolute import - Components must always use " "relative imports within the integration.\n" "Change:\n" - ' from esphome.components.abc import abc_ns"\n' + f" {import_line}\n" "to:\n" - " from . import abc_ns\n\n" + f" {newline}\n" ) @@ -572,18 +595,20 @@ def lint_namespace(fname, content): expected_name = re.match( r"^esphome/components/([^/]+)/.*", fname.replace(os.path.sep, "/") ).group(1) - search = f"namespace {expected_name}" - if search in content: + # Check for both old style and C++17 nested namespace syntax + search_old = f"namespace {expected_name}" + search_new = f"namespace esphome::{expected_name}" + if search_old in content or search_new in content: return None return ( "Invalid namespace found in C++ file. All integration C++ files should put all " "functions in a separate namespace that matches the integration's name. " - f"Please make sure the file contains {highlight(search)}" + f"Please make sure the file contains {highlight(search_old)} or {highlight(search_new)}" ) @lint_content_find_check('"esphome.h"', include=cpp_include, exclude=["tests/custom.h"]) -def lint_esphome_h(fname): +def lint_esphome_h(fname, line, col, content): return ( "File contains reference to 'esphome.h' - This file is " "auto-generated and should only be used for *custom* " @@ -674,7 +699,7 @@ def lint_trailing_whitespace(fname, match): "tests/custom.h", ], ) -def lint_log_in_header(fname): +def lint_log_in_header(fname, line, col, content): return ( "Found reference to ESP_LOG in header file. Using ESP_LOG* in header files " "is currently not possible - please move the definition to a source file (.cpp)" diff --git a/script/clang-format b/script/clang-format index b1e84a56b7..028d752c55 100755 --- a/script/clang-format +++ b/script/clang-format @@ -31,7 +31,11 @@ def run_format(executable, args, queue, lock, failed_files): invocation.append(path) proc = subprocess.run( - invocation, capture_output=True, encoding="utf-8", check=False + invocation, + capture_output=True, + encoding="utf-8", + check=False, + close_fds=False, ) if proc.returncode != 0: with lock: @@ -66,9 +70,10 @@ def main(): ) args = parser.parse_args() - files = [] - for path in git_ls_files(["*.cpp", "*.h", "*.tcc"]): - files.append(os.path.relpath(path, os.getcwd())) + cwd = os.getcwd() + files = [ + os.path.relpath(path, cwd) for path in git_ls_files(["*.cpp", "*.h", "*.tcc"]) + ] if args.files: # Match against files specified on command-line diff --git a/script/clang-tidy b/script/clang-tidy index 5baaaf6b3a..142b616119 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, ) @@ -104,7 +105,7 @@ def clang_options(idedata): # idedata contains include directories for all toolchains of this platform, only use those from the one in use toolchain_dir = os.path.normpath(f"{idedata['cxx_path']}/../../") for directory in idedata["includes"]["toolchain"]: - if directory.startswith(toolchain_dir): + if directory.startswith(toolchain_dir) and "picolibc" not in directory: cmd.extend(["-isystem", directory]) # add library include directories using -isystem to suppress their errors @@ -157,7 +158,11 @@ def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files): invocation.extend(options) proc = subprocess.run( - invocation, capture_output=True, encoding="utf-8", check=False + invocation, + capture_output=True, + encoding="utf-8", + check=False, + close_fds=False, ) if proc.returncode != 0: with lock: @@ -204,7 +209,12 @@ def main(): parser.add_argument( "-c", "--changed", action="store_true", help="only run on changed files" ) - parser.add_argument("-g", "--grep", help="only run on files containing value") + parser.add_argument( + "-g", + "--grep", + action="append", + help="only run on files containing value", + ) parser.add_argument( "--split-num", type=int, help="split the files into X jobs.", default=None ) @@ -218,12 +228,12 @@ def main(): ) args = parser.parse_args() - idedata = load_idedata(args.environment) - options = clang_options(idedata) + cwd = os.getcwd() + files = [os.path.relpath(path, cwd) for path in git_ls_files(["*.cpp"])] - 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 @@ -240,10 +250,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: @@ -296,9 +324,11 @@ def main(): print("Applying fixes ...") try: try: - subprocess.call(["clang-apply-replacements-18", tmpdir]) + subprocess.call( + ["clang-apply-replacements-18", tmpdir], close_fds=False + ) except FileNotFoundError: - subprocess.call(["clang-apply-replacements", tmpdir]) + subprocess.call(["clang-apply-replacements", tmpdir], close_fds=False) except FileNotFoundError: print( "Error please install clang-apply-replacements-18 or clang-apply-replacements.\n", diff --git a/script/clang_tidy_hash.py b/script/clang_tidy_hash.py 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/generate-esp32-boards.py b/script/generate-esp32-boards.py index 83d0f0c3e0..3f444ed455 100755 --- a/script/generate-esp32-boards.py +++ b/script/generate-esp32-boards.py @@ -64,8 +64,10 @@ def main(): for line in lines: if line.startswith("BOARDS = {"): f.write("BOARDS = {\n") - for board, info in sorted(boards.items()): - f.write(TEMPLATE % (board, info["name"], info["variant"])) + f.writelines( + TEMPLATE % (board, info["name"], info["variant"]) + for board, info in sorted(boards.items()) + ) f.write("}\n") break diff --git a/script/helpers.py b/script/helpers.py index 1a0349e434..2c2f44a513 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,102 @@ 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: + # Try gh pr diff first (faster for small PRs) + cmd = ["gh", "pr", "diff", pr_number, "--name-only"] + try: + return _get_changed_files_from_command(cmd) + except Exception as e: + # If it fails due to the 300 file limit, use the API method + if "maximum" in str(e) and "files" in str(e): + cmd = [ + "gh", + "api", + f"repos/esphome/esphome/pulls/{pr_number}/files", + "--paginate", + "--jq", + ".[].filename", + ] + return _get_changed_files_from_command(cmd) + # Re-raise for other errors + raise + + # For pushes (including squash-and-merge) + elif event_name == "push": + # 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,35 +195,175 @@ 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: list[str]) -> list[str]: matched = [] for file in files: with open(file, encoding="utf-8") as handle: contents = handle.read() - if value in contents: + if any(v in contents for v in value): matched.append(file) 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,13 +373,18 @@ 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 - if not platformio_ini.is_file() or not temp_idedata.is_file(): - changed = True - elif platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime: + if ( + not platformio_ini.is_file() + or not temp_idedata.is_file() + or platformio_ini.stat().st_mtime >= temp_idedata.stat().st_mtime + ): changed = True if "idf" in environment: @@ -142,7 +399,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 +418,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 +459,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 +491,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/helpers_zephyr.py b/script/helpers_zephyr.py index c3ba149005..922f1171b4 100644 --- a/script/helpers_zephyr.py +++ b/script/helpers_zephyr.py @@ -25,6 +25,7 @@ int main() { return 0;} Path(zephyr_dir / "prj.conf").write_text( """ CONFIG_NEWLIB_LIBC=y +CONFIG_ADC=y """, encoding="utf-8", ) @@ -41,12 +42,12 @@ CONFIG_NEWLIB_LIBC=y return include_paths def extract_defines(command): - defines = [] define_pattern = re.compile(r"-D\s*([^\s]+)") - for match in define_pattern.findall(command): - if match not in ("_ASMLANGUAGE"): - defines.append(match) - return defines + return [ + match + for match in define_pattern.findall(command) + if match not in ("_ASMLANGUAGE") + ] def find_cxx_path(commands): for entry in commands: @@ -55,6 +56,7 @@ CONFIG_NEWLIB_LIBC=y if not cxx_path.endswith("++"): continue return cxx_path + return None def get_builtin_include_paths(compiler): result = subprocess.run( @@ -78,14 +80,14 @@ CONFIG_NEWLIB_LIBC=y return include_paths def extract_cxx_flags(command): - flags = [] # Extracts CXXFLAGS from the command string, excluding includes and defines. flag_pattern = re.compile( r"(-O[0-3s]|-g|-std=[^\s]+|-Wall|-Wextra|-Werror|--[^\s]+|-f[^\s]+|-m[^\s]+|-imacros\s*[^\s]+)" ) - for match in flag_pattern.findall(command): - flags.append(match.replace("-imacros ", "-imacros")) - return flags + return [ + match.replace("-imacros ", "-imacros") + for match in flag_pattern.findall(command) + ] def transform_to_idedata_format(compile_commands): cxx_path = find_cxx_path(compile_commands) 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/platformio_install_deps.py b/script/platformio_install_deps.py index ed133ecb47..8f7261efc3 100755 --- a/script/platformio_install_deps.py +++ b/script/platformio_install_deps.py @@ -55,4 +55,6 @@ for section in config.sections(): tools.append("-t") tools.append(tool) -subprocess.check_call(["platformio", "pkg", "install", "-g", *libs, *platforms, *tools]) +subprocess.check_call( + ["platformio", "pkg", "install", "-g", *libs, *platforms, *tools], close_fds=False +) diff --git a/script/run-in-env.py b/script/run-in-env.py index d9bd01a62f..886e65db27 100755 --- a/script/run-in-env.py +++ b/script/run-in-env.py @@ -13,7 +13,7 @@ def find_and_activate_virtualenv(): try: # Get the top-level directory of the git repository my_path = subprocess.check_output( - ["git", "rev-parse", "--show-toplevel"], text=True + ["git", "rev-parse", "--show-toplevel"], text=True, close_fds=False ).strip() except subprocess.CalledProcessError: print( @@ -44,7 +44,7 @@ def find_and_activate_virtualenv(): def run_command(): # Execute the remaining arguments in the new environment if len(sys.argv) > 1: - subprocess.run(sys.argv[1:], check=False) + subprocess.run(sys.argv[1:], check=False, close_fds=False) else: print( "No command provided to run in the virtual environment.", diff --git a/script/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/config_validation/test_config.py b/tests/component_tests/config_validation/test_config.py new file mode 100644 index 0000000000..1a9b9bc1f3 --- /dev/null +++ b/tests/component_tests/config_validation/test_config.py @@ -0,0 +1,51 @@ +""" +Test schema.extend functionality in esphome.config_validation. +""" + +from typing import Any + +import esphome.config_validation as cv + + +def test_config_extend() -> None: + """Test that schema.extend correctly merges schemas with extras.""" + + def func1(data: dict[str, Any]) -> dict[str, Any]: + data["extra_1"] = "value1" + return data + + def func2(data: dict[str, Any]) -> dict[str, Any]: + data["extra_2"] = "value2" + return data + + schema1 = cv.Schema( + { + cv.Required("key1"): cv.string, + } + ) + schema1.add_extra(func1) + schema2 = cv.Schema( + { + cv.Required("key2"): cv.string, + } + ) + schema2.add_extra(func2) + extended_schema = schema1.extend(schema2) + config = { + "key1": "initial_value1", + "key2": "initial_value2", + } + validated = extended_schema(config) + assert validated["key1"] == "initial_value1" + assert validated["key2"] == "initial_value2" + assert validated["extra_1"] == "value1" + assert validated["extra_2"] == "value2" + + # Check the opposite order of extension + extended_schema = schema2.extend(schema1) + + validated = extended_schema(config) + assert validated["key1"] == "initial_value1" + assert validated["key2"] == "initial_value2" + assert validated["extra_1"] == "value1" + assert validated["extra_2"] == "value2" diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py index b1e0eaa200..2045b03502 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,60 @@ 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, + full_config: dict[str, 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(full_config or 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 +126,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 +135,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..91e96f24d6 --- /dev/null +++ b/tests/component_tests/esp32/test_esp32.py @@ -0,0 +1,111 @@ +""" +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 CONF_ESPHOME, PlatformFramework +from tests.component_tests.types import SetCoreConfigCallable + + +def test_esp32_config( + set_core_config: SetCoreConfigCallable, +) -> 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", + ), + pytest.param( + { + "variant": "esp32s2", + "framework": { + "type": "esp-idf", + "advanced": {"execute_from_psram": True}, + }, + }, + r"'execute_from_psram' is only supported on ESP32S3 variant @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]", + id="execute_from_psram_invalid_for_variant_config", + ), + pytest.param( + { + "variant": "esp32s3", + "framework": { + "type": "esp-idf", + "advanced": {"execute_from_psram": True}, + }, + }, + r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]", + id="execute_from_psram_requires_psram_config", + ), + pytest.param( + { + "variant": "esp32s3", + "framework": { + "type": "esp-idf", + "advanced": {"ignore_efuse_mac_crc": True}, + }, + }, + r"'ignore_efuse_mac_crc' is not supported on ESP32S3 @ data\['framework'\]\['advanced'\]\['ignore_efuse_mac_crc'\]", + id="ignore_efuse_mac_crc_only_on_esp32", + ), + ], +) +def test_esp32_configuration_errors( + config: Any, + error_match: str, + set_core_config: SetCoreConfigCallable, +) -> None: + set_core_config(PlatformFramework.ESP32_IDF, full_config={CONF_ESPHOME: {}}) + """Test detection of invalid configuration.""" + from esphome.components.esp32 import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA + + with pytest.raises(cv.Invalid, match=error_match): + FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) diff --git a/tests/component_tests/image/__init__.py b/tests/component_tests/image/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/image/config/image_test.yaml b/tests/component_tests/image/config/image_test.yaml index 3ff1260bd0..c34e0993a5 100644 --- a/tests/component_tests/image/config/image_test.yaml +++ b/tests/component_tests/image/config/image_test.yaml @@ -5,10 +5,12 @@ esp32: board: esp32s3box image: - - file: image.png - byte_order: little_endian - id: cat_img + defaults: type: rgb565 + byte_order: little_endian + images: + - file: image.png + id: cat_img spi: mosi_pin: 6 diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py index d8a883d32f..f0b132cef8 100644 --- a/tests/component_tests/image/test_init.py +++ b/tests/component_tests/image/test_init.py @@ -9,7 +9,8 @@ from typing import Any import pytest from esphome import config_validation as cv -from esphome.components.image import CONFIG_SCHEMA +from esphome.components.image import CONF_TRANSPARENCY, CONFIG_SCHEMA +from esphome.const import CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE @pytest.mark.parametrize( @@ -22,12 +23,12 @@ from esphome.components.image import CONFIG_SCHEMA ), pytest.param( {"id": "image_id", "type": "rgb565"}, - r"required key not provided @ data\[0\]\['file'\]", + r"required key not provided @ data\['file'\]", id="missing_file", ), pytest.param( {"file": "image.png", "type": "rgb565"}, - r"required key not provided @ data\[0\]\['id'\]", + r"required key not provided @ data\['id'\]", id="missing_id", ), pytest.param( @@ -160,13 +161,66 @@ def test_image_configuration_errors( }, id="type_based_organization", ), + pytest.param( + { + "defaults": { + "type": "binary", + "transparency": "chroma_key", + "byte_order": "little_endian", + "dither": "FloydSteinberg", + "resize": "100x100", + "invert_alpha": False, + }, + "rgb565": { + "alpha_channel": [ + { + "id": "image_id", + "file": "image.png", + "transparency": "alpha_channel", + "dither": "none", + } + ] + }, + "binary": [ + { + "id": "image_id", + "file": "image.png", + "transparency": "opaque", + } + ], + }, + id="type_based_with_defaults", + ), + pytest.param( + { + "defaults": { + "type": "rgb565", + "transparency": "alpha_channel", + }, + "binary": { + "opaque": [ + { + "id": "image_id", + "file": "image.png", + } + ], + }, + }, + id="binary_with_defaults", + ), ], ) def test_image_configuration_success( config: dict[str, Any] | list[dict[str, Any]], ) -> None: """Test successful configuration validation.""" - CONFIG_SCHEMA(config) + result = CONFIG_SCHEMA(config) + # All valid configurations should return a list of images + assert isinstance(result, list) + for key in (CONF_TYPE, CONF_ID, CONF_TRANSPARENCY, CONF_RAW_DATA_ID): + assert all(key in x for x in result), ( + f"Missing key {key} in image configuration" + ) def test_image_generation( diff --git a/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml new file mode 100644 index 0000000000..7d1fc84121 --- /dev/null +++ b/tests/component_tests/mipi_dsi/fixtures/mipi_dsi.yaml @@ -0,0 +1,28 @@ +esphome: + name: p4-test + +esp32: + board: esp32-p4-evboard + framework: + type: esp-idf + +psram: + speed: 200MHz + +esp_ldo: + - channel: 3 + voltage: 2.5V + +display: + - platform: mipi_dsi + model: WAVESHARE-P4-NANO-10.1 + +i2c: + sda: GPIO7 + scl: GPIO8 + scan: true + frequency: 400kHz + +#light: + #- platform: mipi_dsi + #id: backlight_id diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py new file mode 100644 index 0000000000..f8a9af0279 --- /dev/null +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -0,0 +1,127 @@ +"""Tests for mpi_dsi configuration validation.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome import config_validation as cv +from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32P4 +from esphome.const import ( + CONF_DIMENSIONS, + CONF_HEIGHT, + CONF_INIT_SEQUENCE, + CONF_WIDTH, + KEY_VARIANT, + PlatformFramework, +) +from tests.component_tests.types import SetCoreConfigCallable + + +def test_configuration_errors(set_core_config: SetCoreConfigCallable) -> None: + """Test detection of invalid configuration""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-p4-evboard", KEY_VARIANT: VARIANT_ESP32P4}, + ) + + from esphome.components.mipi_dsi.display import CONFIG_SCHEMA + + with pytest.raises(cv.Invalid, match="expected a dictionary"): + CONFIG_SCHEMA("a string") + + with pytest.raises( + cv.Invalid, match=r"required key not provided @ data\['model'\]" + ): + CONFIG_SCHEMA({"id": "display_id"}) + + with pytest.raises( + cv.Invalid, + match=r"string value is None for dictionary value @ data\['lane_bit_rate'\]", + ): + CONFIG_SCHEMA( + {"id": "display_id", "model": "custom", "init_sequence": [[0x36, 0x01]]} + ) + + with pytest.raises( + cv.Invalid, match=r"required key not provided @ data\['dimensions'\]" + ): + CONFIG_SCHEMA( + { + "id": "display_id", + "model": "custom", + "init_sequence": [[0x36, 0x01]], + "lane_bit_rate": "1.5Gbps", + } + ) + + with pytest.raises( + cv.Invalid, match=r"required key not provided @ data\['init_sequence'\]" + ): + CONFIG_SCHEMA( + { + "model": "custom", + "lane_bit_rate": "1.5Gbps", + "dimensions": {"width": 320, "height": 240}, + } + ) + + +def test_configuration_success(set_core_config: SetCoreConfigCallable) -> None: + """Test successful configuration validation.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-p4-evboard", KEY_VARIANT: VARIANT_ESP32P4}, + ) + + from esphome.components.mipi_dsi.display import CONFIG_SCHEMA, MODELS + + # Custom model with all options + CONFIG_SCHEMA( + { + "model": "custom", + "pixel_mode": "16bit", + "id": "display_id", + "byte_order": "little_endian", + "color_order": "rgb", + "reset_pin": 12, + "init_sequence": [[0xA0, 0x01]], + "dimensions": { + "width": 320, + "height": 240, + }, + "invert_colors": True, + "transform": {"mirror_x": True, "mirror_y": True}, + "pclk_frequency": "40MHz", + "lane_bit_rate": "1.5Gbps", + "lanes": 2, + "use_axis_flips": True, + } + ) + + # Test all models, providing default values where necessary + for name, model in MODELS.items(): + config = {"model": name} + if model.initsequence is None: + config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]] + if not model.get_default(CONF_DIMENSIONS): + config[CONF_DIMENSIONS] = {CONF_WIDTH: 400, CONF_HEIGHT: 300} + if not model.get_default("lane_bit_rate"): + config["lane_bit_rate"] = "1.5Gbps" + CONFIG_SCHEMA(config) + + +def test_code_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("mipi_dsi.yaml")) + assert ( + "mipi_dsi_mipi_dsi_id = new mipi_dsi::MIPI_DSI(800, 1280, display::COLOR_BITNESS_565, 16);" + in main_cpp + ) + assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp + assert "mipi_dsi_mipi_dsi_id->set_lane_bit_rate(1500);" in main_cpp + # assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp 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/conftest.py b/tests/component_tests/mipi_spi/conftest.py new file mode 100644 index 0000000000..c3070c7965 --- /dev/null +++ b/tests/component_tests/mipi_spi/conftest.py @@ -0,0 +1,43 @@ +"""Tests for mpip_spi configuration validation.""" + +from collections.abc import Callable, Generator + +import pytest + +from esphome import config_validation as cv +from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANTS +from esphome.components.esp32.gpio import validate_gpio_pin +from esphome.const import CONF_INPUT, CONF_OUTPUT +from esphome.core import CORE +from esphome.pins import gpio_pin_schema + + +@pytest.fixture +def choose_variant_with_pins() -> Generator[Callable[[list], 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: list) -> None: + for v in VARIANTS: + try: + CORE.data[KEY_ESP32][KEY_VARIANT] = v + for pin in pins: + if pin is not None: + pin = gpio_pin_schema( + { + CONF_INPUT: True, + CONF_OUTPUT: True, + }, + internal=True, + )(pin) + validate_gpio_pin(pin) + return + except cv.Invalid: + continue + raise cv.Invalid( + f"No compatible variant found for pins: {', '.join(map(str, pins))}" + ) + + yield chooser 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..fbb3222812 --- /dev/null +++ b/tests/component_tests/mipi_spi/test_init.py @@ -0,0 +1,360 @@ +"""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_VARIANT, + VARIANT_ESP32, + VARIANT_ESP32S3, +) +from esphome.components.mipi import CONF_NATIVE_HEIGHT +from esphome.components.mipi_spi.display import ( + CONF_BUS_MODE, + 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.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.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 framework\(s\) 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[[list], 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..ee9d317339 --- /dev/null +++ b/tests/component_tests/types.py @@ -0,0 +1,22 @@ +"""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, + full_config: dict[str, 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/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-p4-idf.yaml b/tests/components/adc/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..97844cf398 --- /dev/null +++ b/tests/components/adc/test.esp32-p4-idf.yaml @@ -0,0 +1,6 @@ +packages: + base: !include common.yaml + +sensor: + - id: !extend my_sensor + pin: GPIO50 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.nrf52-adafruit.yaml b/tests/components/adc/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..4be5b4b5c2 --- /dev/null +++ b/tests/components/adc/test.nrf52-adafruit.yaml @@ -0,0 +1,23 @@ +sensor: + - platform: adc + pin: VDDHDIV5 + name: "VDDH Voltage" + update_interval: 5sec + filters: + - multiply: 5 + - platform: adc + pin: VDD + name: "VDD Voltage" + update_interval: 5sec + - platform: adc + pin: AIN0 + name: "AIN0 Voltage" + update_interval: 5sec + - platform: adc + pin: P0.03 + name: "AIN1 Voltage" + update_interval: 5sec + - platform: adc + name: "AIN2 Voltage" + update_interval: 5sec + pin: 4 diff --git a/tests/components/adc/test.nrf52-mcumgr.yaml b/tests/components/adc/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..4be5b4b5c2 --- /dev/null +++ b/tests/components/adc/test.nrf52-mcumgr.yaml @@ -0,0 +1,23 @@ +sensor: + - platform: adc + pin: VDDHDIV5 + name: "VDDH Voltage" + update_interval: 5sec + filters: + - multiply: 5 + - platform: adc + pin: VDD + name: "VDD Voltage" + update_interval: 5sec + - platform: adc + pin: AIN0 + name: "AIN0 Voltage" + update_interval: 5sec + - platform: adc + pin: P0.03 + name: "AIN1 Voltage" + update_interval: 5sec + - platform: adc + name: "AIN2 Voltage" + update_interval: 5sec + pin: 4 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/ade7880/common.yaml b/tests/components/ade7880/common.yaml index 48c22c8485..0aa388a325 100644 --- a/tests/components/ade7880/common.yaml +++ b/tests/components/ade7880/common.yaml @@ -12,12 +12,12 @@ sensor: frequency: 60Hz phase_a: name: Channel A - voltage: Channel A Voltage - current: Channel A Current - active_power: Channel A Active Power - power_factor: Channel A Power Factor - forward_active_energy: Channel A Forward Active Energy - reverse_active_energy: Channel A Reverse Active Energy + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy calibration: current_gain: 3116628 voltage_gain: -757178 @@ -25,12 +25,12 @@ sensor: phase_angle: 188 phase_b: name: Channel B - voltage: Channel B Voltage - current: Channel B Current - active_power: Channel B Active Power - power_factor: Channel B Power Factor - forward_active_energy: Channel B Forward Active Energy - reverse_active_energy: Channel B Reverse Active Energy + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy calibration: current_gain: 3133655 voltage_gain: -755235 @@ -38,12 +38,12 @@ sensor: phase_angle: 188 phase_c: name: Channel C - voltage: Channel C Voltage - current: Channel C Current - active_power: Channel C Active Power - power_factor: Channel C Power Factor - forward_active_energy: Channel C Forward Active Energy - reverse_active_energy: Channel C Reverse Active Energy + voltage: Voltage + current: Current + active_power: Active Power + power_factor: Power Factor + forward_active_energy: Forward Active Energy + reverse_active_energy: Reverse Active Energy calibration: current_gain: 3111158 voltage_gain: -743813 @@ -51,6 +51,6 @@ sensor: phase_angle: 180 neutral: name: Neutral - current: Neutral Current + current: Current calibration: current_gain: 3189 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/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/debug/test.nrf52-adafruit.yaml b/tests/components/debug/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.nrf52-mcumgr.yaml b/tests/components/debug/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml 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/test.esp32-s3-idf.yaml b/tests/components/esp32/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..1d5a5e52a4 --- /dev/null +++ b/tests/components/esp32/test.esp32-s3-idf.yaml @@ -0,0 +1,12 @@ +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + execute_from_psram: true + +psram: + mode: octal + speed: 80MHz + +logger: 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/esp32_touch/common-get-value.yaml b/tests/components/esp32_touch/common-get-value.yaml new file mode 100644 index 0000000000..4066303797 --- /dev/null +++ b/tests/components/esp32_touch/common-get-value.yaml @@ -0,0 +1,18 @@ +binary_sensor: + - platform: esp32_touch + name: ESP32 Touch Pad Get Value + pin: ${pin} + threshold: 1000 + id: esp32_touch_pad_get_value + on_press: + then: + - lambda: |- + // Test that get_value() compiles and works + uint32_t value = id(esp32_touch_pad_get_value).get_value(); + ESP_LOGD("test", "Touch value on press: %u", value); + on_release: + then: + - lambda: |- + // Test get_value() on release + uint32_t value = id(esp32_touch_pad_get_value).get_value(); + ESP_LOGD("test", "Touch value on release: %u", value); diff --git a/tests/components/esp32_touch/test.esp32-idf.yaml b/tests/components/esp32_touch/test.esp32-idf.yaml index 25316b8646..5158613eb1 100644 --- a/tests/components/esp32_touch/test.esp32-idf.yaml +++ b/tests/components/esp32_touch/test.esp32-idf.yaml @@ -2,3 +2,4 @@ substitutions: pin: GPIO27 <<: !include common.yaml +<<: !include common-get-value.yaml diff --git a/tests/components/esp32_touch/test.esp32-s2-idf.yaml b/tests/components/esp32_touch/test.esp32-s2-idf.yaml index 575d758fae..b9f5671969 100644 --- a/tests/components/esp32_touch/test.esp32-s2-idf.yaml +++ b/tests/components/esp32_touch/test.esp32-s2-idf.yaml @@ -2,3 +2,4 @@ substitutions: pin: GPIO12 <<: !include common-variants.yaml +<<: !include common-get-value.yaml diff --git a/tests/components/esp32_touch/test.esp32-s3-idf.yaml b/tests/components/esp32_touch/test.esp32-s3-idf.yaml index 575d758fae..b9f5671969 100644 --- a/tests/components/esp32_touch/test.esp32-s3-idf.yaml +++ b/tests/components/esp32_touch/test.esp32-s3-idf.yaml @@ -2,3 +2,4 @@ substitutions: pin: GPIO12 <<: !include common-variants.yaml +<<: !include common-get-value.yaml diff --git a/tests/components/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/espnow/common.yaml b/tests/components/espnow/common.yaml new file mode 100644 index 0000000000..abb31c12b8 --- /dev/null +++ b/tests/components/espnow/common.yaml @@ -0,0 +1,52 @@ +espnow: + auto_add_peer: false + channel: 1 + peers: + - 11:22:33:44:55:66 + on_receive: + - logger.log: + format: "Received from: %s = '%s' RSSI: %d" + args: + - format_mac_address_pretty(info.src_addr).c_str() + - format_hex_pretty(data, size).c_str() + - info.rx_ctrl->rssi + - espnow.send: + address: 11:22:33:44:55:66 + data: "Hello from ESPHome" + on_sent: + - logger.log: "ESPNow message sent successfully" + on_error: + - logger.log: "ESPNow message failed to send" + wait_for_sent: true + continue_on_error: true + + - espnow.send: + address: 11:22:33:44:55:66 + data: [0x01, 0x02, 0x03, 0x04, 0x05] + - espnow.send: + address: 11:22:33:44:55:66 + data: !lambda 'return {0x01, 0x02, 0x03, 0x04, 0x05};' + - espnow.broadcast: + data: "Hello, World!" + - espnow.broadcast: + data: [0x01, 0x02, 0x03, 0x04, 0x05] + - espnow.broadcast: + data: !lambda 'return {0x01, 0x02, 0x03, 0x04, 0x05};' + - espnow.peer.add: + address: 11:22:33:44:55:66 + - espnow.peer.delete: + address: 11:22:33:44:55:66 + on_broadcast: + - logger.log: + format: "Broadcast from: %s = '%s' RSSI: %d" + args: + - format_mac_address_pretty(info.src_addr).c_str() + - format_hex_pretty(data, size).c_str() + - info.rx_ctrl->rssi + on_unknown_peer: + - logger.log: + format: "Unknown peer: %s = '%s' RSSI: %d" + args: + - format_mac_address_pretty(info.src_addr).c_str() + - format_hex_pretty(data, size).c_str() + - info.rx_ctrl->rssi diff --git a/tests/components/espnow/test.esp32-idf.yaml b/tests/components/espnow/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/espnow/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/factory_reset/common.yaml b/tests/components/factory_reset/common.yaml index ad3abd603e..7617dc4b81 100644 --- a/tests/components/factory_reset/common.yaml +++ b/tests/components/factory_reset/common.yaml @@ -1,3 +1,7 @@ button: - platform: factory_reset name: Reset to Factory Default Settings + +factory_reset: + resets_required: 5 + max_delay: 10s diff --git a/tests/components/factory_reset/test.bk72xx-ard.yaml b/tests/components/factory_reset/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/factory_reset/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/factory_reset/test.esp8266-ard.yaml b/tests/components/factory_reset/test.esp8266-ard.yaml index dade44d145..591a319b81 100644 --- a/tests/components/factory_reset/test.esp8266-ard.yaml +++ b/tests/components/factory_reset/test.esp8266-ard.yaml @@ -1 +1,4 @@ +esp8266: + restore_from_flash: true + <<: !include common.yaml diff --git a/tests/components/factory_reset/test.rp2040-ard.yaml b/tests/components/factory_reset/test.rp2040-ard.yaml index dade44d145..ad3abd603e 100644 --- a/tests/components/factory_reset/test.rp2040-ard.yaml +++ b/tests/components/factory_reset/test.rp2040-ard.yaml @@ -1 +1,3 @@ -<<: !include common.yaml +button: + - platform: factory_reset + name: Reset to Factory Default Settings 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/gpio/test.nrf52-adafruit.yaml b/tests/components/gpio/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..912b9537c4 --- /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: P0.3 + id: gpio_output + +switch: + - platform: gpio + pin: P1.2 + 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..912b9537c4 --- /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: P0.3 + id: gpio_output + +switch: + - platform: gpio + pin: P1.2 + id: gpio_switch diff --git a/tests/components/gps/test.esp32-c3-idf.yaml b/tests/components/gps/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/gps/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/gps/test.esp32-idf.yaml b/tests/components/gps/test.esp32-idf.yaml new file mode 100644 index 0000000000..811f6b72a6 --- /dev/null +++ b/tests/components/gps/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO12 + rx_pin: GPIO14 + +<<: !include common.yaml diff --git a/tests/components/host/common.yaml b/tests/components/host/common.yaml index fca0c5d597..5c329c8245 100644 --- a/tests/components/host/common.yaml +++ b/tests/components/host/common.yaml @@ -8,3 +8,10 @@ logger: host: mac_address: "62:23:45:AF:B3:DD" + +esphome: + on_boot: + - lambda: |- + static const uint8_t my_addr[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + if (!mac_address_is_valid(my_addr)) + ESP_LOGD("test", "Invalid mac address %X", my_addr[0]); // etc. diff --git a/tests/components/ld2412/common.yaml b/tests/components/ld2412/common.yaml new file mode 100644 index 0000000000..9176c61fd5 --- /dev/null +++ b/tests/components/ld2412/common.yaml @@ -0,0 +1,233 @@ +uart: + - id: uart_ld2412 + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + +ld2412: + id: my_ld2412 + +binary_sensor: + - platform: ld2412 + dynamic_background_correction_status: + name: Dynamic Background Correction Status + has_target: + name: Presence + has_moving_target: + name: Moving Target + has_still_target: + name: Still Target + +button: + - platform: ld2412 + factory_reset: + name: Factory reset + restart: + name: Restart + query_params: + name: Query params + start_dynamic_background_correction: + name: Start Dynamic Background Correction + +number: + - platform: ld2412 + light_threshold: + name: Light Threshold + timeout: + name: Presence timeout + min_distance_gate: + name: Minimum distance gate + max_distance_gate: + name: Maximum distance gate + gate_0: + move_threshold: + name: Gate 0 Move Threshold + still_threshold: + name: Gate 0 Still Threshold + gate_1: + move_threshold: + name: Gate 1 Move Threshold + still_threshold: + name: Gate 1 Still Threshold + gate_2: + move_threshold: + name: Gate 2 Move Threshold + still_threshold: + name: Gate 2 Still Threshold + gate_3: + move_threshold: + name: Gate 3 Move Threshold + still_threshold: + name: Gate 3 Still Threshold + gate_4: + move_threshold: + name: Gate 4 Move Threshold + still_threshold: + name: Gate 4 Still Threshold + gate_5: + move_threshold: + name: Gate 5 Move Threshold + still_threshold: + name: Gate 5 Still Threshold + gate_6: + move_threshold: + name: Gate 6 Move Threshold + still_threshold: + name: Gate 6 Still Threshold + gate_7: + move_threshold: + name: Gate 7 Move Threshold + still_threshold: + name: Gate 7 Still Threshold + gate_8: + move_threshold: + name: Gate 8 Move Threshold + still_threshold: + name: Gate 8 Still Threshold + gate_9: + move_threshold: + name: Gate 9 Move Threshold + still_threshold: + name: Gate 9 Still Threshold + gate_10: + move_threshold: + name: Gate 10 Move Threshold + still_threshold: + name: Gate 10 Still Threshold + gate_11: + move_threshold: + name: Gate 11 Move Threshold + still_threshold: + name: Gate 11 Still Threshold + gate_12: + move_threshold: + name: Gate 12 Move Threshold + still_threshold: + name: Gate 12 Still Threshold + gate_13: + move_threshold: + name: Gate 13 Move Threshold + still_threshold: + name: Gate 13 Still Threshold + +select: + - platform: ld2412 + light_function: + name: Light Function + out_pin_level: + name: Hardware output pin level + distance_resolution: + name: Distance resolution + baud_rate: + name: Baud rate + on_value: + - delay: 3s + - lambda: |- + id(uart_ld2412).flush(); + uint32_t new_baud_rate = stoi(x); + ESP_LOGD("change_baud_rate", "Changing baud rate from %i to %i",id(uart_ld2412).get_baud_rate(), new_baud_rate); + if (id(uart_ld2412).get_baud_rate() != new_baud_rate) { + id(uart_ld2412).set_baud_rate(new_baud_rate); + #if defined(USE_ESP8266) || defined(USE_ESP32) + id(uart_ld2412).load_settings(); + #endif + } + +sensor: + - platform: ld2412 + light: + name: Light + moving_distance: + name: Moving Distance + still_distance: + name: Still Distance + moving_energy: + name: Move Energy + still_energy: + name: Still Energy + detection_distance: + name: Detection Distance + gate_0: + move_energy: + name: Gate 0 Move Energy + still_energy: + name: Gate 0 Still Energy + gate_1: + move_energy: + name: Gate 1 Move Energy + still_energy: + name: Gate 1 Still Energy + gate_2: + move_energy: + name: Gate 2 Move Energy + still_energy: + name: Gate 2 Still Energy + gate_3: + move_energy: + name: Gate 3 Move Energy + still_energy: + name: Gate 3 Still Energy + gate_4: + move_energy: + name: Gate 4 Move Energy + still_energy: + name: Gate 4 Still Energy + gate_5: + move_energy: + name: Gate 5 Move Energy + still_energy: + name: Gate 5 Still Energy + gate_6: + move_energy: + name: Gate 6 Move Energy + still_energy: + name: Gate 6 Still Energy + gate_7: + move_energy: + name: Gate 7 Move Energy + still_energy: + name: Gate 7 Still Energy + gate_8: + move_energy: + name: Gate 8 Move Energy + still_energy: + name: Gate 8 Still Energy + gate_9: + move_energy: + name: Gate 9 Move Energy + still_energy: + name: Gate 9 Still Energy + gate_10: + move_energy: + name: Gate 10 Move Energy + still_energy: + name: Gate 10 Still Energy + gate_11: + move_energy: + name: Gate 11 Move Energy + still_energy: + name: Gate 11 Still Energy + gate_12: + move_energy: + name: Gate 12 Move Energy + still_energy: + name: Gate 12 Still Energy + gate_13: + move_energy: + name: Gate 13 Move Energy + still_energy: + name: Gate 13 Still Energy + +switch: + - platform: ld2412 + bluetooth: + name: Bluetooth + engineering_mode: + name: Engineering Mode + +text_sensor: + - platform: ld2412 + version: + name: Firmware version + mac_address: + name: MAC address diff --git a/tests/components/ld2412/test.esp32-ard.yaml b/tests/components/ld2412/test.esp32-ard.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/ld2412/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp32-c3-ard.yaml b/tests/components/ld2412/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2412/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp32-c3-idf.yaml b/tests/components/ld2412/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2412/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp32-idf.yaml b/tests/components/ld2412/test.esp32-idf.yaml new file mode 100644 index 0000000000..f486544afa --- /dev/null +++ b/tests/components/ld2412/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp8266-ard.yaml b/tests/components/ld2412/test.esp8266-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2412/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2412/test.rp2040-ard.yaml b/tests/components/ld2412/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/ld2412/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ld2450/common.yaml b/tests/components/ld2450/common.yaml index 2e62efb0f5..c18bed46b0 100644 --- a/tests/components/ld2450/common.yaml +++ b/tests/components/ld2450/common.yaml @@ -9,7 +9,6 @@ uart: ld2450: - id: ld2450_radar uart_id: ld2450_uart - throttle: 1000ms button: - platform: ld2450 diff --git a/tests/components/logger/test.nrf52-adafruit.yaml b/tests/components/logger/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..70b485daac --- /dev/null +++ b/tests/components/logger/test.nrf52-adafruit.yaml @@ -0,0 +1,7 @@ +esphome: + on_boot: + then: + - logger.log: Hello world + +logger: + level: DEBUG diff --git a/tests/components/logger/test.nrf52-mcumgr.yaml b/tests/components/logger/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..70b485daac --- /dev/null +++ b/tests/components/logger/test.nrf52-mcumgr.yaml @@ -0,0 +1,7 @@ +esphome: + on_boot: + then: + - logger.log: Hello world + +logger: + level: DEBUG diff --git a/tests/components/lvgl/.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 853466c9cc..feee96672c 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -78,7 +78,7 @@ lvgl: - id: date_style text_font: roboto10 align: center - text_color: color_id2 + text_color: !lambda return color_id2; bg_opa: cover radius: 4 pad_all: 2 @@ -723,6 +723,20 @@ lvgl: arc_color: 0xFFFF00 focused: arc_color: 0x808080 + - arc: + align: center + id: lv_arc_1 + value: !lambda return 75; + min_value: !lambda return 50; + max_value: !lambda return 60; + arc_color: 0xFF0000 + indicator: + arc_width: !lambda return 20; + arc_color: 0xF000FF + pressed: + arc_color: 0xFFFF00 + focused: + arc_color: 0x808080 - bar: id: bar_id align: top_mid 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_dsi/test.esp32-p4-idf.yaml b/tests/components/mipi_dsi/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..9c4eb07d9b --- /dev/null +++ b/tests/components/mipi_dsi/test.esp32-p4-idf.yaml @@ -0,0 +1,21 @@ +esp_ldo: + - id: ldo_id + channel: 3 + voltage: 2.5V + +display: + - platform: mipi_dsi + model: JC1060P470 + enable_pin: GPIO22 + +#light: + #- platform: mipi_dsi + #id: backlight_id + +psram: + +i2c: + sda: GPIO7 + scl: GPIO8 + scan: true + frequency: 400kHz 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/output/common.yaml b/tests/components/output/common.yaml index 5f31ae50a8..81d802e9bf 100644 --- a/tests/components/output/common.yaml +++ b/tests/components/output/common.yaml @@ -6,6 +6,12 @@ esphome: - output.set_level: id: light_output_1 level: 50% + - output.set_min_power: + id: light_output_1 + min_power: 20% + - output.set_max_power: + id: light_output_1 + max_power: 80% output: - platform: ${output_platform} diff --git a/tests/components/packages/garage-door.yaml b/tests/components/packages/garage-door.yaml new file mode 100644 index 0000000000..e16265d1e1 --- /dev/null +++ b/tests/components/packages/garage-door.yaml @@ -0,0 +1,5 @@ +switch: + - name: ${door_name} Garage Door Switch + platform: gpio + pin: ${door_pin} + id: ${door_id} diff --git a/tests/components/packages/test-vars.esp32-idf.yaml b/tests/components/packages/test-vars.esp32-idf.yaml new file mode 100644 index 0000000000..f12467d9f9 --- /dev/null +++ b/tests/components/packages/test-vars.esp32-idf.yaml @@ -0,0 +1,19 @@ +packages: + left_garage_door: !include + file: garage-door.yaml + vars: + door_name: Left + door_pin: 1 + door_id: left_garage_door + middle_garage_door: !include + file: garage-door.yaml + vars: + door_name: Middle + door_pin: 2 + door_id: middle_garage_door + right_garage_door: !include + file: garage-door.yaml + vars: + door_name: Right + door_pin: 3 + door_id: right_garage_door 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/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index 29f48d995d..3be4bf3cca 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -204,3 +204,9 @@ button: command: 0xEC rc_code_1: 0x0D rc_code_2: 0x0D + - platform: template + name: Digital Write + on_press: + - remote_transmitter.digital_write: true + - remote_transmitter.digital_write: + value: false 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/switch/common.yaml b/tests/components/switch/common.yaml index 8d6972f91b..afdf26c150 100644 --- a/tests/components/switch/common.yaml +++ b/tests/components/switch/common.yaml @@ -9,3 +9,23 @@ switch: name: "Template Switch" id: the_switch optimistic: true + on_state: + - if: + condition: + - lambda: return x; + then: + - logger.log: "Switch turned ON" + else: + - logger.log: "Switch turned OFF" + on_turn_on: + - logger.log: "Switch is now ON" + on_turn_off: + - logger.log: "Switch is now OFF" + +esphome: + on_boot: + - switch.turn_on: the_switch + - switch.turn_off: the_switch + - switch.control: + id: the_switch + state: !lambda return (1 > 2); diff --git a/tests/components/template/common.yaml b/tests/components/template/common.yaml index fd9342b3e5..ae7dc98e57 100644 --- a/tests/components/template/common.yaml +++ b/tests/components/template/common.yaml @@ -1,44 +1,3 @@ -sensor: - - platform: template - name: "Template Sensor" - id: template_sens - lambda: |- - if (id(some_binary_sensor).state) { - return 42.0; - } else { - return 0.0; - } - update_interval: 60s - filters: - - offset: 10 - - multiply: 1 - - offset: !lambda return 10; - - multiply: !lambda return 2; - - filter_out: - - 10 - - 20 - - !lambda return 10; - - filter_out: 10 - - filter_out: !lambda return NAN; - - timeout: - timeout: 10s - value: !lambda return 10; - - timeout: - timeout: 1h - value: 20.0 - - timeout: - timeout: 1d - - to_ntc_resistance: - calibration: - - 10.0kOhm -> 25°C - - 27.219kOhm -> 0°C - - 14.674kOhm -> 15°C - - to_ntc_temperature: - calibration: - - 10.0kOhm -> 25°C - - 27.219kOhm -> 0°C - - 14.674kOhm -> 15°C - esphome: on_boot: - sensor.template.publish: @@ -82,6 +41,133 @@ binary_sensor: sensor.in_range: id: template_sens below: 30.0 + filters: + - invert: + - delayed_on: 100ms + - delayed_off: 100ms + - delayed_on_off: !lambda "if (id(test_switch).state) return 1000; else return 0;" + - delayed_on_off: + time_on: 10s + time_off: !lambda "if (id(test_switch).state) return 1000; else return 0;" + - autorepeat: + - delay: 1s + time_off: 100ms + time_on: 900ms + - delay: 5s + time_off: 100ms + time_on: 400ms + - lambda: |- + if (id(other_binary_sensor).state) { + return x; + } else { + return {}; + } + - settle: 500ms + - timeout: 5s + +sensor: + - platform: template + name: "Template Sensor" + id: template_sens + lambda: |- + if (id(some_binary_sensor).state) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + filters: + - calibrate_linear: + - 0.0 -> 0.0 + - 40.0 -> 45.0 + - 100.0 -> 102.5 + - calibrate_polynomial: + degree: 2 + datapoints: + # Map 0.0 (from sensor) to 0.0 (true value) + - 0.0 -> 0.0 + - 10.0 -> 12.1 + - 13.0 -> 14.0 + - clamp: + max_value: 10.0 + min_value: -10.0 + - debounce: 0.1s + - delta: 5.0 + - exponential_moving_average: + alpha: 0.1 + send_every: 15 + - filter_out: + - 10 + - 20 + - !lambda return 10; + - filter_out: 10 + - filter_out: !lambda return NAN; + - heartbeat: 5s + - lambda: return x * (9.0/5.0) + 32.0; + - max: + window_size: 10 + send_every: 2 + send_first_at: 1 + - median: + window_size: 7 + send_every: 4 + send_first_at: 3 + - min: + window_size: 10 + send_every: 2 + send_first_at: 1 + - multiply: 1 + - multiply: !lambda return 2; + - offset: 10 + - offset: !lambda return 10; + - or: + - quantile: + window_size: 7 + send_every: 4 + send_first_at: 3 + quantile: .9 + - round: 1 + - round_to_multiple_of: 0.25 + - skip_initial: 3 + - sliding_window_moving_average: + window_size: 15 + send_every: 15 + - throttle: 1s + - throttle_average: 2s + - throttle_with_priority: 5s + - throttle_with_priority: + timeout: 3s + value: 42.0 + - throttle_with_priority: + timeout: 3s + value: !lambda return 1.0f / 2.0f; + - throttle_with_priority: + timeout: 3s + value: + - 42.0 + - !lambda return 2.0f / 2.0f; + - nan + - timeout: + timeout: 10s + value: !lambda return 10; + - timeout: + timeout: 1h + value: 20.0 + - timeout: + timeout: 1min + value: last + - timeout: + timeout: 1d + - to_ntc_resistance: + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C + - to_ntc_temperature: + calibration: + - 10.0kOhm -> 25°C + - 27.219kOhm -> 0°C + - 14.674kOhm -> 15°C output: - platform: template @@ -92,6 +178,7 @@ output: switch: - platform: template + id: test_switch name: "Template Switch" lambda: |- if (id(some_binary_sensor).state) { 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/tm1651/test.esp32-c3-idf.yaml b/tests/components/tm1651/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1651/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/tm1651/test.esp32-idf.yaml b/tests/components/tm1651/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/tm1651/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml 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.esp32-idf.yaml b/tests/components/web_server/test.esp32-idf.yaml index 7e6658e20e..24b292d0d6 100644 --- a/tests/components/web_server/test.esp32-idf.yaml +++ b/tests/components/web_server/test.esp32-idf.yaml @@ -1 +1,6 @@ <<: !include common_v2.yaml + +web_server: + auth: + username: admin + password: password 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/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml index dade44d145..91e235b9ce 100644 --- a/tests/components/wifi/test.esp32-idf.yaml +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -1 +1,7 @@ -<<: !include common.yaml +psram: + +wifi: + use_psram: true + +packages: + - !include common.yaml 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/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index cd02200d0b..b77ab7a7a3 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -31,8 +31,7 @@ class DashboardTestHelper: else: url = f"http://127.0.0.1:{self.port}{path}" future = self.client.fetch(url, raise_error=True, **kwargs) - result = await future - return result + return await future @pytest_asyncio.fixture() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index aead6a73af..0530752551 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,67 @@ 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, + close_fds=False, + ) + + # 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.""" @@ -121,19 +184,17 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s content = content.replace("api:", f"api:\n port: {unused_tcp_port}") # Add debug build flags for integration tests to enable assertions - if "esphome:" in content: - # Check if platformio_options already exists - if "platformio_options:" not in content: - # Add platformio_options with debug flags after esphome: - content = content.replace( - "esphome:", - "esphome:\n" - " # Enable assertions for integration tests\n" - " platformio_options:\n" - " build_flags:\n" - ' - "-DDEBUG" # Enable assert() statements\n' - ' - "-g" # Add debug symbols', - ) + if "esphome:" in content and "platformio_options:" not in content: + # Add platformio_options with debug flags after esphome: + content = content.replace( + "esphome:", + "esphome:\n" + " # Enable assertions for integration tests\n" + " platformio_options:\n" + " build_flags:\n" + ' - "-DDEBUG" # Enable assert() statements\n' + ' - "-g" # Add debug symbols', + ) return content @@ -161,22 +222,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 @@ -193,25 +246,25 @@ async def compile_esphome( # Start in a new process group to isolate signal handling start_new_session=True, env=env, + close_fds=False, ) await proc.wait() if proc.returncode == 0: # Success! break - elif proc.returncode == -11 and attempt < max_retries - 1: + if proc.returncode == -11 and attempt < max_retries - 1: # Segfault (-11 = SIGSEGV), retry print( f"Compilation segfaulted (attempt {attempt + 1}/{max_retries}), retrying..." ) await asyncio.sleep(1) # Brief pause before retry continue - else: - # Other error or final retry - raise RuntimeError( - f"Failed to compile {config_path}, return code: {proc.returncode}. " - f"Run with 'pytest -s' to see compilation output." - ) + # Other error or final retry + raise RuntimeError( + f"Failed to compile {config_path}, return code: {proc.returncode}. " + f"Run with 'pytest -s' to see compilation output." + ) # Load the config to get idedata (blocking call, must use executor) loop = asyncio.get_running_loop() @@ -341,7 +394,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 @@ -426,6 +479,7 @@ async def run_binary_and_wait_for_port( # Start in a new process group to isolate signal handling start_new_session=True, pass_fds=(device_fd,), + close_fds=False, ) # Close the device end in the parent process @@ -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_homeassistant.yaml b/tests/integration/fixtures/api_homeassistant.yaml new file mode 100644 index 0000000000..ce8628977a --- /dev/null +++ b/tests/integration/fixtures/api_homeassistant.yaml @@ -0,0 +1,311 @@ +esphome: + name: test-ha-api + friendly_name: Home Assistant API Test + +host: + +api: + services: + - service: trigger_all_tests + then: + - logger.log: "=== Starting Home Assistant API Tests ===" + - button.press: test_basic_service + - button.press: test_templated_service + - button.press: test_empty_string_service + - button.press: test_multiple_fields_service + - button.press: test_complex_lambda_service + - button.press: test_all_empty_service + - button.press: test_rapid_service_calls + - button.press: test_read_ha_states + - number.set: + id: ha_number + value: 42.5 + - switch.turn_on: ha_switch + - switch.turn_off: ha_switch + - logger.log: "=== All tests completed ===" + +logger: + level: DEBUG + +# Time component for templated values +time: + - platform: homeassistant + id: homeassistant_time + +# Global variables for testing +globals: + - id: test_brightness + type: int + initial_value: '75' + - id: test_string + type: std::string + initial_value: '"test_value"' + +# Sensors for testing state reading +sensor: + - platform: template + name: "Test Sensor" + id: test_sensor + lambda: return 42.0; + update_interval: 0.1s + + # Home Assistant sensor that reads external state + - platform: homeassistant + name: "HA Temperature" + entity_id: sensor.external_temperature + id: ha_temperature + on_value: + then: + - logger.log: + format: "HA Temperature state updated: %.1f" + args: ['x'] + + # Test multiple HA state sensors + - platform: homeassistant + name: "HA Humidity" + entity_id: sensor.external_humidity + id: ha_humidity + on_value: + then: + - logger.log: + format: "HA Humidity state updated: %.1f" + args: ['x'] + +# Binary sensor from Home Assistant +binary_sensor: + - platform: homeassistant + name: "HA Motion" + entity_id: binary_sensor.external_motion + id: ha_motion + on_state: + then: + - logger.log: + format: "HA Motion state changed: %s" + args: ['x ? "ON" : "OFF"'] + +# Text sensor from Home Assistant +text_sensor: + - platform: homeassistant + name: "HA Weather" + entity_id: weather.home + attribute: condition + id: ha_weather + on_value: + then: + - logger.log: + format: "HA Weather condition updated: %s" + args: ['x.c_str()'] + + # Test empty state handling + - platform: homeassistant + name: "HA Empty State" + entity_id: sensor.nonexistent_sensor + id: ha_empty_state + on_value: + then: + - logger.log: + format: "HA Empty state updated: %s" + args: ['x.c_str()'] + +# Number component for testing HA number control +number: + - platform: template + name: "HA Controlled Number" + id: ha_number + min_value: 0 + max_value: 100 + step: 1 + optimistic: true + set_action: + - logger.log: + format: "Setting HA number to: %.1f" + args: ['x'] + - homeassistant.action: + action: input_number.set_value + data: + entity_id: input_number.test_number + value: !lambda 'return to_string(x);' + +# Switch component for testing HA switch control +switch: + - platform: template + name: "HA Controlled Switch" + id: ha_switch + optimistic: true + turn_on_action: + - logger.log: "Toggling HA switch: switch.test_switch ON" + - homeassistant.action: + action: switch.turn_on + data: + entity_id: switch.test_switch + turn_off_action: + - logger.log: "Toggling HA switch: switch.test_switch OFF" + - homeassistant.action: + action: switch.turn_off + data: + entity_id: switch.test_switch + +# Buttons for testing various service call scenarios +button: + # Test 1: Basic service call with static values + - platform: template + name: "Test Basic Service" + id: test_basic_service + on_press: + - logger.log: "Sending HomeAssistant service call: light.turn_off" + - homeassistant.action: + action: light.turn_off + data: + entity_id: light.test_light + - logger.log: "Service data: entity_id=light.test_light" + + # Test 2: Service call with templated/lambda values (main bug fix test) + - platform: template + name: "Test Templated Service" + id: test_templated_service + on_press: + - logger.log: "Testing templated service call" + - lambda: |- + int brightness_percent = id(test_brightness); + std::string computed = to_string(brightness_percent * 255 / 100); + ESP_LOGI("test", "Lambda computed value: %s", computed.c_str()); + - homeassistant.action: + action: light.turn_on + data: + entity_id: light.test_light + # This creates a temporary string - the main test case + brightness: !lambda 'return to_string(id(test_brightness) * 255 / 100);' + data_template: + color_name: !lambda 'return id(test_string);' + variables: + transition: !lambda 'return "2.5";' + + # Test 3: Service call with empty string values + - platform: template + name: "Test Empty String Service" + id: test_empty_string_service + on_press: + - logger.log: "Testing empty string values" + - homeassistant.action: + action: notify.test + data: + message: "Test message" + title: "" + data_template: + target: !lambda 'return "";' + variables: + sound: !lambda 'return "";' + + - logger.log: "Empty value for key: title" + - logger.log: "Empty value for key: target" + - logger.log: "Empty value for key: sound" + + # Test 4: Service call with multiple data fields + - platform: template + name: "Test Multiple Fields Service" + id: test_multiple_fields_service + on_press: + - logger.log: "Testing multiple data fields" + - homeassistant.action: + action: climate.set_temperature + data: + entity_id: climate.test_climate + temperature: "22" + hvac_mode: "heat" + data_template: + target_temp_high: !lambda 'return "24";' + target_temp_low: !lambda 'return "20";' + variables: + preset_mode: !lambda 'return "comfort";' + + # Test 5: Complex lambda with string operations + - platform: template + name: "Test Complex Lambda Service" + id: test_complex_lambda_service + on_press: + - logger.log: "Testing complex lambda expressions" + - homeassistant.action: + action: script.test_script + data: + entity_id: !lambda |- + std::string base = "light."; + std::string room = "living_room"; + return base + room; + brightness_pct: !lambda |- + float sensor_val = id(test_sensor).state; + int pct = (int)(sensor_val * 2.38); // 42 * 2.38 ≈ 100 + return to_string(pct); + data_template: + message: !lambda |- + char buffer[50]; + snprintf(buffer, sizeof(buffer), "Sensor: %.1f, Time: %02d:%02d", + id(test_sensor).state, + id(homeassistant_time).now().hour, + id(homeassistant_time).now().minute); + return std::string(buffer); + + # Test 6: Service with only empty strings to verify size calculation + - platform: template + name: "Test All Empty Service" + id: test_all_empty_service + on_press: + - logger.log: "Testing all empty string values" + - homeassistant.action: + action: test.empty + data: + field1: "" + field2: "" + data_template: + field3: !lambda 'return "";' + variables: + field4: !lambda 'return "";' + - logger.log: "All empty service call completed" + + # Test 7: Rapid successive service calls + - platform: template + name: "Test Rapid Service Calls" + id: test_rapid_service_calls + on_press: + - logger.log: "Testing rapid service calls" + - repeat: + count: 5 + then: + - homeassistant.action: + action: counter.increment + data: + entity_id: counter.test_counter + - delay: 10ms + - logger.log: "Rapid service calls completed" + + # Test 8: Log current HA states + - platform: template + name: "Test Read HA States" + id: test_read_ha_states + on_press: + - logger.log: "Reading current HA states" + - lambda: |- + if (id(ha_temperature).has_state()) { + ESP_LOGI("test", "Current HA Temperature: %.1f", id(ha_temperature).state); + } else { + ESP_LOGI("test", "HA Temperature has no state"); + } + + if (id(ha_humidity).has_state()) { + ESP_LOGI("test", "Current HA Humidity: %.1f", id(ha_humidity).state); + } else { + ESP_LOGI("test", "HA Humidity has no state"); + } + + ESP_LOGI("test", "Current HA Motion: %s", id(ha_motion).state ? "ON" : "OFF"); + + if (id(ha_weather).has_state()) { + ESP_LOGI("test", "Current HA Weather: %s", id(ha_weather).state.c_str()); + } else { + ESP_LOGI("test", "HA Weather has no state"); + } + + if (id(ha_empty_state).has_state()) { + ESP_LOGI("test", "HA Empty State value: %s", id(ha_empty_state).state.c_str()); + } else { + ESP_LOGI("test", "HA Empty State has no value (expected)"); + } 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/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/gpio_expander_test_component/__init__.py b/tests/integration/fixtures/external_components/gpio_expander_test_component/__init__.py new file mode 100644 index 0000000000..5672f80004 --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +AUTO_LOAD = ["gpio_expander"] + +gpio_expander_test_component_ns = cg.esphome_ns.namespace( + "gpio_expander_test_component" +) + +GPIOExpanderTestComponent = gpio_expander_test_component_ns.class_( + "GPIOExpanderTestComponent", cg.Component +) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(GPIOExpanderTestComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp b/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp new file mode 100644 index 0000000000..7e88950592 --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp @@ -0,0 +1,38 @@ +#include "gpio_expander_test_component.h" + +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +namespace esphome::gpio_expander_test_component { + +static const char *const TAG = "gpio_expander_test"; + +void GPIOExpanderTestComponent::setup() { + for (uint8_t pin = 0; pin < 32; pin++) { + this->digital_read(pin); + } + + this->digital_read(3); + this->digital_read(3); + this->digital_read(4); + this->digital_read(3); + this->digital_read(10); + this->reset_pin_cache_(); // Reset cache to ensure next read is from hardware + this->digital_read(15); + this->digital_read(14); + this->digital_read(14); + + ESP_LOGD(TAG, "DONE"); +} + +bool GPIOExpanderTestComponent::digital_read_hw(uint8_t pin) { + ESP_LOGD(TAG, "digital_read_hw pin=%d", pin); + return true; +} + +bool GPIOExpanderTestComponent::digital_read_cache(uint8_t pin) { + ESP_LOGD(TAG, "digital_read_cache pin=%d", pin); + return true; +} + +} // namespace esphome::gpio_expander_test_component diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.h b/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.h new file mode 100644 index 0000000000..ffaee2cd65 --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/core/component.h" + +namespace esphome::gpio_expander_test_component { + +class GPIOExpanderTestComponent : public Component, public esphome::gpio_expander::CachedGpioExpander { + public: + void setup() override; + + protected: + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override{}; +}; + +} // namespace esphome::gpio_expander_test_component diff --git a/tests/integration/fixtures/external_components/loop_test_component/__init__.py b/tests/integration/fixtures/external_components/loop_test_component/__init__.py index 3f3a40db09..a0b0f8c65a 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/__init__.py +++ b/tests/integration/fixtures/external_components/loop_test_component/__init__.py @@ -72,8 +72,7 @@ DisableAction = loop_test_component_ns.class_("DisableAction", automation.Action ) async def enable_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, parent) - return var + return cg.new_Pvariable(action_id, template_arg, parent) @automation.register_action( @@ -87,8 +86,7 @@ async def enable_to_code(config, action_id, template_arg, args): ) async def disable_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, parent) - return var + return cg.new_Pvariable(action_id, template_arg, parent) async def to_code(config): diff --git a/tests/integration/fixtures/gpio_expander_cache.yaml b/tests/integration/fixtures/gpio_expander_cache.yaml new file mode 100644 index 0000000000..7d7ca1a876 --- /dev/null +++ b/tests/integration/fixtures/gpio_expander_cache.yaml @@ -0,0 +1,17 @@ +esphome: + name: gpio-expander-cache +host: + +logger: + level: DEBUG + +api: + +# External component that uses gpio_expander::CachedGpioExpander +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [gpio_expander_test_component] + +gpio_expander_test_component: 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/host_mode_many_entities.yaml b/tests/integration/fixtures/host_mode_many_entities.yaml index 3d1aa36196..612186507c 100644 --- a/tests/integration/fixtures/host_mode_many_entities.yaml +++ b/tests/integration/fixtures/host_mode_many_entities.yaml @@ -210,6 +210,15 @@ sensor: name: "Test Sensor 50" lambda: return 50.0; update_interval: 0.1s + # Temperature sensor for the thermostat + - platform: template + name: "Temperature Sensor" + id: temp_sensor + lambda: return 22.5; + unit_of_measurement: "°C" + device_class: temperature + state_class: measurement + update_interval: 5s # Mixed entity types for comprehensive batching test binary_sensor: @@ -285,6 +294,50 @@ valve: stop_action: - logger.log: "Valve stopping" +output: + - platform: template + id: heater_output + type: binary + write_action: + - logger.log: "Heater output changed" + - platform: template + id: cooler_output + type: binary + write_action: + - logger.log: "Cooler output changed" + +climate: + - platform: thermostat + name: "Test Thermostat" + sensor: temp_sensor + default_preset: Home + on_boot_restore_from: default_preset + min_heating_off_time: 1s + min_heating_run_time: 1s + min_cooling_off_time: 1s + min_cooling_run_time: 1s + min_idle_time: 1s + heat_action: + - output.turn_on: heater_output + cool_action: + - output.turn_on: cooler_output + idle_action: + - output.turn_off: heater_output + - output.turn_off: cooler_output + preset: + - name: Home + default_target_temperature_low: 20 + default_target_temperature_high: 24 + mode: heat_cool + - name: Away + default_target_temperature_low: 16 + default_target_temperature_high: 26 + mode: heat_cool + - name: Sleep + default_target_temperature_low: 18 + default_target_temperature_high: 22 + mode: heat_cool + alarm_control_panel: - platform: template name: "Test Alarm" @@ -320,3 +373,20 @@ button: name: "Test Button" on_press: - logger.log: "Button pressed" + +# Date, Time, and DateTime entities +datetime: + - platform: template + type: date + name: "Test Date" + initial_value: "2023-05-13" + optimistic: true + - platform: template + type: time + name: "Test Time" + initial_value: "12:30:00" + optimistic: true + - platform: template + type: datetime + name: "Test DateTime" + optimistic: true diff --git a/tests/integration/fixtures/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/runtime_stats.yaml b/tests/integration/fixtures/runtime_stats.yaml new file mode 100644 index 0000000000..aad1c275fb --- /dev/null +++ b/tests/integration/fixtures/runtime_stats.yaml @@ -0,0 +1,39 @@ +esphome: + name: runtime-stats-test + +host: + +api: + +logger: + level: DEBUG + logs: + runtime_stats: INFO + +runtime_stats: + log_interval: 1s + +# Add some components that will execute periodically to generate stats +sensor: + - platform: template + name: "Test Sensor 1" + id: test_sensor_1 + lambda: return 42.0; + update_interval: 0.1s + + - platform: template + name: "Test Sensor 2" + id: test_sensor_2 + lambda: return 24.0; + update_interval: 0.2s + +switch: + - platform: template + name: "Test Switch" + id: test_switch + optimistic: true + +interval: + - interval: 0.5s + then: + - switch.toggle: test_switch diff --git a/tests/integration/fixtures/scheduler_retry_test.yaml b/tests/integration/fixtures/scheduler_retry_test.yaml new file mode 100644 index 0000000000..11fff6c395 --- /dev/null +++ b/tests/integration/fixtures/scheduler_retry_test.yaml @@ -0,0 +1,307 @@ +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: VERY_VERBOSE + +globals: + - id: simple_retry_counter + type: int + initial_value: '0' + - id: backoff_retry_counter + type: int + initial_value: '0' + - id: backoff_last_attempt_time + type: uint32_t + 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' + - id: const_char_retry_counter + type: int + initial_value: '0' + - id: static_char_retry_counter + type: int + initial_value: '0' + - id: mixed_cancel_result + type: bool + initial_value: 'false' + +# Using different component types for each test to ensure isolation +sensor: + - platform: template + name: Simple Retry Test Sensor + id: simple_retry_sensor + lambda: return 1.0; + update_interval: never + + - platform: template + name: Backoff Retry Test Sensor + id: backoff_retry_sensor + lambda: return 2.0; + update_interval: never + + - platform: template + name: Immediate Done Test Sensor + id: immediate_done_sensor + lambda: return 3.0; + update_interval: never + +binary_sensor: + - platform: template + name: Cancel Retry Test Binary Sensor + id: cancel_retry_binary_sensor + lambda: return false; + + - platform: template + name: Empty Name Test Binary Sensor + id: empty_name_binary_sensor + lambda: return true; + +switch: + - platform: template + name: Script Retry Test Switch + id: script_retry_switch + optimistic: true + + - platform: template + name: Multiple Same Name Test Switch + id: multiple_same_name_switch + optimistic: true + +script: + - id: run_all_tests + then: + # Test 1: Simple retry + - logger.log: "=== Test 1: Simple retry ===" + - lambda: |- + auto *component = id(simple_retry_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(backoff_retry_sensor); + + App.scheduler.set_retry(component, "backoff_retry", 50, 4, + [](uint8_t retry_countdown) { + id(backoff_retry_counter)++; + uint32_t now = millis(); + uint32_t interval = 0; + + // Only calculate interval after first attempt + if (id(backoff_retry_counter) > 1) { + interval = now - id(backoff_last_attempt_time); + } + id(backoff_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(immediate_done_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(cancel_retry_binary_sensor); + App.scheduler.set_retry(component, "cancel_test", 30, 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(cancel_retry_binary_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(empty_name_binary_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(empty_name_binary_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(multiple_same_name_switch); + + // 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; + }); + + # Test 8: Const char* overloads + - logger.log: "=== Test 8: Const char* overloads ===" + - lambda: |- + auto *component = id(simple_retry_sensor); + + // Test 8a: Direct string literal + App.scheduler.set_retry(component, "const_char_test", 30, 2, + [](uint8_t retry_countdown) { + id(const_char_retry_counter)++; + ESP_LOGI("test", "Const char retry %d", id(const_char_retry_counter)); + return RetryResult::DONE; + }); + + # Test 9: Static const char* variable + - logger.log: "=== Test 9: Static const char* ===" + - lambda: |- + auto *component = id(backoff_retry_sensor); + + static const char* STATIC_NAME = "static_retry_test"; + App.scheduler.set_retry(component, STATIC_NAME, 20, 1, + [](uint8_t retry_countdown) { + id(static_char_retry_counter)++; + ESP_LOGI("test", "Static const char retry %d", id(static_char_retry_counter)); + return RetryResult::DONE; + }); + + // Cancel with same static const char* + App.scheduler.set_timeout(component, "static_cancel", 10, []() { + static const char* STATIC_NAME = "static_retry_test"; + bool result = App.scheduler.cancel_retry(id(backoff_retry_sensor), STATIC_NAME); + ESP_LOGI("test", "Static cancel result: %s", result ? "true" : "false"); + }); + + # Test 10: Mix string and const char* cancel + - logger.log: "=== Test 10: Mixed string/const char* ===" + - lambda: |- + auto *component = id(immediate_done_sensor); + + // Set with std::string + std::string str_name = "mixed_retry"; + App.scheduler.set_retry(component, str_name, 40, 3, + [](uint8_t retry_countdown) { + ESP_LOGI("test", "Mixed retry - should be cancelled"); + return RetryResult::RETRY; + }); + + // Cancel with const char* + id(mixed_cancel_result) = App.scheduler.cancel_retry(component, "mixed_retry"); + ESP_LOGI("test", "Mixed cancel result: %s", id(mixed_cancel_result) ? "true" : "false"); + + # 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 2-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", "Const char retry counter: %d (expected 1)", id(const_char_retry_counter)); + ESP_LOGI("test", "Static char retry counter: %d (expected 1)", id(static_char_retry_counter)); + ESP_LOGI("test", "Mixed cancel result: %s (expected true)", id(mixed_cancel_result) ? "true" : "false"); + ESP_LOGI("test", "All retry tests completed"); diff --git a/tests/integration/test_api_custom_services.py b/tests/integration/test_api_custom_services.py index 2862dcc900..9ae4cdcb5d 100644 --- a/tests/integration/test_api_custom_services.py +++ b/tests/integration/test_api_custom_services.py @@ -59,86 +59,86 @@ async def test_api_custom_services( 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" + 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-custom-services-test" - # List services - _, services = await client.list_entities_services() + # List services + _, services = await client.list_entities_services() - # Should have 4 services: 1 YAML + 3 CustomAPIDevice - assert len(services) == 4, f"Expected 4 services, found {len(services)}" + # Should have 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 + # 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 + 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" - ) + 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 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) + # 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 + # 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) + # 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 + # 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) + # 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_homeassistant.py b/tests/integration/test_api_homeassistant.py new file mode 100644 index 0000000000..f69838396d --- /dev/null +++ b/tests/integration/test_api_homeassistant.py @@ -0,0 +1,305 @@ +"""Integration test for Home Assistant API functionality. + +Tests: +- Home Assistant service calls with templated values (main bug fix) +- Service calls with empty string values +- Home Assistant state reading (sensors, binary sensors, text sensors) +- Home Assistant number and switch component control +- Complex lambda expressions and string handling +""" + +from __future__ import annotations + +import asyncio +import re + +from aioesphomeapi import HomeassistantServiceCall +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_homeassistant( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Comprehensive test for Home Assistant API functionality.""" + loop = asyncio.get_running_loop() + + # Create futures for patterns that capture values + lambda_computed_future = loop.create_future() + ha_temp_state_future = loop.create_future() + ha_humidity_state_future = loop.create_future() + ha_motion_state_future = loop.create_future() + ha_weather_state_future = loop.create_future() + + # State update futures + temp_update_future = loop.create_future() + humidity_update_future = loop.create_future() + motion_update_future = loop.create_future() + weather_update_future = loop.create_future() + + # Number future + ha_number_future = loop.create_future() + + tests_complete_future = loop.create_future() + + # Patterns to match in logs - only keeping patterns that capture values + lambda_computed_pattern = re.compile(r"Lambda computed value: (\d+)") + ha_temp_state_pattern = re.compile(r"Current HA Temperature: ([\d.]+)") + ha_humidity_state_pattern = re.compile(r"Current HA Humidity: ([\d.]+)") + ha_motion_state_pattern = re.compile(r"Current HA Motion: (ON|OFF)") + ha_weather_state_pattern = re.compile(r"Current HA Weather: (\w+)") + + # State update patterns + temp_update_pattern = re.compile(r"HA Temperature state updated: ([\d.]+)") + humidity_update_pattern = re.compile(r"HA Humidity state updated: ([\d.]+)") + motion_update_pattern = re.compile(r"HA Motion state changed: (ON|OFF)") + weather_update_pattern = re.compile(r"HA Weather condition updated: (\w+)") + + # Number pattern + ha_number_pattern = re.compile(r"Setting HA number to: ([\d.]+)") + + tests_complete_pattern = re.compile(r"=== All tests completed ===") + + # Track all log lines for debugging + log_lines: list[str] = [] + + # Track HomeAssistant service calls + ha_service_calls: list[HomeassistantServiceCall] = [] + + # Service call futures organized by service name + service_call_futures = { + "light.turn_off": loop.create_future(), # basic_service_call + "light.turn_on": loop.create_future(), # templated_service_call + "notify.test": loop.create_future(), # empty_string_service_call + "climate.set_temperature": loop.create_future(), # multiple_fields_service_call + "script.test_script": loop.create_future(), # complex_lambda_service_call + "test.empty": loop.create_future(), # all_empty_service_call + "input_number.set_value": loop.create_future(), # ha_number_service_call + "switch.turn_on": loop.create_future(), # ha_switch_on_service_call + "switch.turn_off": loop.create_future(), # ha_switch_off_service_call + } + + def on_service_call(service_call: HomeassistantServiceCall) -> None: + """Capture HomeAssistant service calls.""" + ha_service_calls.append(service_call) + + # Check if this service call is one we're waiting for + if service_call.service in service_call_futures: + future = service_call_futures[service_call.service] + if not future.done(): + future.set_result(service_call) + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + log_lines.append(line) + + # Check for patterns that capture values + if not lambda_computed_future.done(): + match = lambda_computed_pattern.search(line) + if match: + lambda_computed_future.set_result(match.group(1)) + elif not ha_temp_state_future.done() and ha_temp_state_pattern.search(line): + ha_temp_state_future.set_result(line) + elif not ha_humidity_state_future.done() and ha_humidity_state_pattern.search( + line + ): + ha_humidity_state_future.set_result(line) + elif not ha_motion_state_future.done() and ha_motion_state_pattern.search(line): + ha_motion_state_future.set_result(line) + elif not ha_weather_state_future.done() and ha_weather_state_pattern.search( + line + ): + ha_weather_state_future.set_result(line) + + # Check state update patterns + elif not temp_update_future.done() and temp_update_pattern.search(line): + temp_update_future.set_result(line) + elif not humidity_update_future.done() and humidity_update_pattern.search(line): + humidity_update_future.set_result(line) + elif not motion_update_future.done() and motion_update_pattern.search(line): + motion_update_future.set_result(line) + elif not weather_update_future.done() and weather_update_pattern.search(line): + weather_update_future.set_result(line) + + # Check number pattern + elif not ha_number_future.done() and ha_number_pattern.search(line): + match = ha_number_pattern.search(line) + if match: + ha_number_future.set_result(match.group(1)) + + elif not tests_complete_future.done() and tests_complete_pattern.search(line): + tests_complete_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 == "test-ha-api" + + # Subscribe to HomeAssistant service calls + client.subscribe_service_calls(on_service_call) + + # Send some Home Assistant states for our sensors to read + client.send_home_assistant_state("sensor.external_temperature", "", "22.5") + client.send_home_assistant_state("sensor.external_humidity", "", "65.0") + client.send_home_assistant_state("binary_sensor.external_motion", "", "ON") + client.send_home_assistant_state("weather.home", "condition", "sunny") + + # List entities and services + _, services = await client.list_entities_services() + + # Find the trigger service + trigger_service = next( + (s for s in services if s.name == "trigger_all_tests"), None + ) + assert trigger_service is not None, "trigger_all_tests service not found" + + # Execute all tests + client.execute_service(trigger_service, {}) + + # Wait for all tests to complete with appropriate timeouts + try: + # Templated service test - the main bug fix + computed_value = await asyncio.wait_for(lambda_computed_future, timeout=5.0) + # Verify the computed value is reasonable (75 * 255 / 100 = 191.25 -> 191) + assert computed_value in ["191", "192"], ( + f"Unexpected computed value: {computed_value}" + ) + + # Check state reads - verify we received the mocked values + temp_line = await asyncio.wait_for(ha_temp_state_future, timeout=5.0) + assert "Current HA Temperature: 22.5" in temp_line + + humidity_line = await asyncio.wait_for( + ha_humidity_state_future, timeout=5.0 + ) + assert "Current HA Humidity: 65.0" in humidity_line + + motion_line = await asyncio.wait_for(ha_motion_state_future, timeout=5.0) + assert "Current HA Motion: ON" in motion_line + + weather_line = await asyncio.wait_for(ha_weather_state_future, timeout=5.0) + assert "Current HA Weather: sunny" in weather_line + + # Number test + number_value = await asyncio.wait_for(ha_number_future, timeout=5.0) + assert number_value == "42.5", f"Unexpected number value: {number_value}" + + # Wait for completion + await asyncio.wait_for(tests_complete_future, timeout=5.0) + + # Now verify the protobuf messages + # 1. Basic service call + basic_call = await asyncio.wait_for( + service_call_futures["light.turn_off"], timeout=2.0 + ) + assert basic_call.service == "light.turn_off" + assert "entity_id" in basic_call.data, ( + f"entity_id not found in data: {basic_call.data}" + ) + assert basic_call.data["entity_id"] == "light.test_light", ( + f"Wrong entity_id: {basic_call.data['entity_id']}" + ) + + # 2. Templated service call - verify the temporary string issue is fixed + templated_call = await asyncio.wait_for( + service_call_futures["light.turn_on"], timeout=2.0 + ) + assert templated_call.service == "light.turn_on" + # Check the computed brightness value + assert "brightness" in templated_call.data + assert templated_call.data["brightness"] in ["191", "192"] # 75 * 255 / 100 + # Check data_template + assert "color_name" in templated_call.data_template + assert templated_call.data_template["color_name"] == "test_value" + # Check variables + assert "transition" in templated_call.variables + assert templated_call.variables["transition"] == "2.5" + + # 3. Empty string service call + empty_call = await asyncio.wait_for( + service_call_futures["notify.test"], timeout=2.0 + ) + assert empty_call.service == "notify.test" + # Verify empty strings are properly handled + assert "title" in empty_call.data and empty_call.data["title"] == "" + assert ( + "target" in empty_call.data_template + and empty_call.data_template["target"] == "" + ) + assert ( + "sound" in empty_call.variables and empty_call.variables["sound"] == "" + ) + + # 4. Multiple fields service call + multi_call = await asyncio.wait_for( + service_call_futures["climate.set_temperature"], timeout=2.0 + ) + assert multi_call.service == "climate.set_temperature" + assert multi_call.data["temperature"] == "22" + assert multi_call.data["hvac_mode"] == "heat" + assert multi_call.data_template["target_temp_high"] == "24" + assert multi_call.variables["preset_mode"] == "comfort" + + # 5. Complex lambda service call + complex_call = await asyncio.wait_for( + service_call_futures["script.test_script"], timeout=2.0 + ) + assert complex_call.service == "script.test_script" + assert complex_call.data["entity_id"] == "light.living_room" + assert complex_call.data["brightness_pct"] == "99" # 42 * 2.38 ≈ 99 + # Check message includes sensor value + assert "message" in complex_call.data_template + assert "Sensor: 42.0" in complex_call.data_template["message"] + + # 6. All empty service call + all_empty_call = await asyncio.wait_for( + service_call_futures["test.empty"], timeout=2.0 + ) + assert all_empty_call.service == "test.empty" + # All fields should be empty strings + assert all(v == "" for v in all_empty_call.data.values()) + assert all(v == "" for v in all_empty_call.data_template.values()) + assert all(v == "" for v in all_empty_call.variables.values()) + + # 7. HA Number service call + number_call = await asyncio.wait_for( + service_call_futures["input_number.set_value"], timeout=2.0 + ) + assert number_call.service == "input_number.set_value" + assert number_call.data["entity_id"] == "input_number.test_number" + # The value might be formatted with trailing zeros + assert float(number_call.data["value"]) == 42.5 + + # 8. HA Switch service calls + switch_on_call = await asyncio.wait_for( + service_call_futures["switch.turn_on"], timeout=2.0 + ) + assert switch_on_call.service == "switch.turn_on" + assert switch_on_call.data["entity_id"] == "switch.test_switch" + + switch_off_call = await asyncio.wait_for( + service_call_futures["switch.turn_off"], timeout=2.0 + ) + assert switch_off_call.service == "switch.turn_off" + assert switch_off_call.data["entity_id"] == "switch.test_switch" + + except TimeoutError as e: + # Show recent log lines for debugging + recent_logs = "\n".join(log_lines[-20:]) + service_calls_summary = "\n".join( + f"- {call.service}" for call in ha_service_calls + ) + pytest.fail( + f"Test timed out waiting for expected log pattern or service call. Error: {e}\n\n" + f"Recent log lines:\n{recent_logs}\n\n" + f"Received service calls:\n{service_calls_summary}" + ) 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_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_device_id_in_state.py b/tests/integration/test_device_id_in_state.py index eaa91ec92e..51088bcbf7 100644 --- a/tests/integration/test_device_id_in_state.py +++ b/tests/integration/test_device_id_in_state.py @@ -47,9 +47,7 @@ async def test_device_id_in_state( entity_device_mapping[entity.key] = device_ids["Humidity Monitor"] elif entity.name == "Motion Detected": entity_device_mapping[entity.key] = device_ids["Motion Sensor"] - elif entity.name == "Temperature Monitor Power": - entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] - elif entity.name == "Temperature Status": + elif entity.name in {"Temperature Monitor Power", "Temperature Status"}: entity_device_mapping[entity.key] = device_ids["Temperature Monitor"] elif entity.name == "Motion Light": entity_device_mapping[entity.key] = device_ids["Motion Sensor"] @@ -77,7 +75,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..9b2670c4ae 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() @@ -38,8 +38,7 @@ async def test_duplicate_entities_not_allowed_on_different_devices( # Get entity list entities = await client.list_entities_services() all_entities: list[EntityInfo] = [] - for entity_list in entities[0]: - all_entities.append(entity_list) + all_entities.extend(entities[0]) # Group entities by type for easier testing sensors = [e for e in all_entities if e.__class__.__name__ == "SensorInfo"] @@ -52,46 +51,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 +95,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 +163,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 +176,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_gpio_expander_cache.py b/tests/integration/test_gpio_expander_cache.py new file mode 100644 index 0000000000..9353bb1dd6 --- /dev/null +++ b/tests/integration/test_gpio_expander_cache.py @@ -0,0 +1,123 @@ +"""Integration test for CachedGPIOExpander to ensure correct behavior.""" + +from __future__ import annotations + +import asyncio +from pathlib import Path +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_gpio_expander_cache( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test gpio_expander::CachedGpioExpander correctly calls hardware functions.""" + # Get the path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + logs_done = asyncio.Event() + + # Patterns to match in logs + digital_read_hw_pattern = re.compile(r"digital_read_hw pin=(\d+)") + digital_read_cache_pattern = re.compile(r"digital_read_cache pin=(\d+)") + + # ensure logs are in the expected order + log_order = [ + (digital_read_hw_pattern, 0), + [(digital_read_cache_pattern, i) for i in range(0, 8)], + (digital_read_hw_pattern, 8), + [(digital_read_cache_pattern, i) for i in range(8, 16)], + (digital_read_hw_pattern, 16), + [(digital_read_cache_pattern, i) for i in range(16, 24)], + (digital_read_hw_pattern, 24), + [(digital_read_cache_pattern, i) for i in range(24, 32)], + (digital_read_hw_pattern, 3), + (digital_read_cache_pattern, 3), + (digital_read_hw_pattern, 3), + (digital_read_cache_pattern, 3), + (digital_read_cache_pattern, 4), + (digital_read_hw_pattern, 3), + (digital_read_cache_pattern, 3), + (digital_read_hw_pattern, 10), + (digital_read_cache_pattern, 10), + # full cache reset here for testing + (digital_read_hw_pattern, 15), + (digital_read_cache_pattern, 15), + (digital_read_cache_pattern, 14), + (digital_read_hw_pattern, 14), + (digital_read_cache_pattern, 14), + ] + # Flatten the log order for easier processing + log_order: list[tuple[re.Pattern, int]] = [ + item + for sublist in log_order + for item in (sublist if isinstance(sublist, list) else [sublist]) + ] + + index = 0 + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + nonlocal index + if logs_done.is_set(): + return + + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + + if "digital_read" in clean_line: + if index >= len(log_order): + print(f"Received unexpected log line: {clean_line}") + logs_done.set() + return + + pattern, expected_pin = log_order[index] + match = pattern.search(clean_line) + + if not match: + print(f"Log line did not match next expected pattern: {clean_line}") + logs_done.set() + return + + pin = int(match.group(1)) + if pin != expected_pin: + print(f"Unexpected pin number. Expected {expected_pin}, got {pin}") + logs_done.set() + return + + index += 1 + + elif "DONE" in clean_line: + # Check if we reached the end of the expected log entries + logs_done.set() + + # Run with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "gpio-expander-cache" + + try: + await asyncio.wait_for(logs_done.wait(), timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for logs to complete") + + assert index == len(log_order), ( + f"Expected {len(log_order)} log entries, but got {index}" + ) diff --git a/tests/integration/test_host_mode_api_password.py b/tests/integration/test_host_mode_api_password.py 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_fan_preset.py b/tests/integration/test_host_mode_fan_preset.py index d18b9f08ad..d44fdc0a3b 100644 --- a/tests/integration/test_host_mode_fan_preset.py +++ b/tests/integration/test_host_mode_fan_preset.py @@ -23,9 +23,7 @@ async def test_host_mode_fan_preset( entities = await client.list_entities_services() fans: list[FanInfo] = [] for entity_list in entities: - for entity in entity_list: - if isinstance(entity, FanInfo): - fans.append(entity) + fans.extend(entity for entity in entity_list if isinstance(entity, FanInfo)) # Create a map of fan names to entity info fan_map = {fan.name: fan for fan in fans} diff --git a/tests/integration/test_host_mode_many_entities.py b/tests/integration/test_host_mode_many_entities.py index 19d1ee315f..fbe3dc25c8 100644 --- a/tests/integration/test_host_mode_many_entities.py +++ b/tests/integration/test_host_mode_many_entities.py @@ -4,7 +4,17 @@ from __future__ import annotations import asyncio -from aioesphomeapi import EntityState, SensorState +from aioesphomeapi import ( + ClimateInfo, + DateInfo, + DateState, + DateTimeInfo, + DateTimeState, + EntityState, + SensorState, + TimeInfo, + TimeState, +) import pytest from .types import APIClientConnectedFactory, RunCompiledFunction @@ -22,34 +32,56 @@ async def test_host_mode_many_entities( async with run_compiled(yaml_config), api_client_connected() as client: # Subscribe to state changes states: dict[int, EntityState] = {} - sensor_count_future: asyncio.Future[int] = loop.create_future() + minimum_states_future: asyncio.Future[None] = loop.create_future() def on_state(state: EntityState) -> None: states[state.key] = state - # Count sensor states specifically + # Check if we have received minimum expected states sensor_states = [ s for s in states.values() if isinstance(s, SensorState) and isinstance(s.state, float) ] - # When we have received states from at least 50 sensors, resolve the future - if len(sensor_states) >= 50 and not sensor_count_future.done(): - sensor_count_future.set_result(len(sensor_states)) + date_states = [s for s in states.values() if isinstance(s, DateState)] + time_states = [s for s in states.values() if isinstance(s, TimeState)] + datetime_states = [ + s for s in states.values() if isinstance(s, DateTimeState) + ] + + # We expect at least 50 sensors and 1 of each datetime entity type + if ( + len(sensor_states) >= 50 + and len(date_states) >= 1 + and len(time_states) >= 1 + and len(datetime_states) >= 1 + and not minimum_states_future.done() + ): + minimum_states_future.set_result(None) client.subscribe_states(on_state) - # Wait for states from at least 50 sensors with timeout + # Wait for minimum states with timeout try: - sensor_count = await asyncio.wait_for(sensor_count_future, timeout=10.0) - except asyncio.TimeoutError: + await asyncio.wait_for(minimum_states_future, timeout=10.0) + except TimeoutError: sensor_states = [ s for s in states.values() if isinstance(s, SensorState) and isinstance(s.state, float) ] + date_states = [s for s in states.values() if isinstance(s, DateState)] + time_states = [s for s in states.values() if isinstance(s, TimeState)] + datetime_states = [ + s for s in states.values() if isinstance(s, DateTimeState) + ] + pytest.fail( - f"Did not receive states from at least 50 sensors within 10 seconds. " - f"Received {len(sensor_states)} sensor states out of {len(states)} total states" + f"Did not receive expected states within 10 seconds. " + f"Received: {len(sensor_states)} sensor states (expected >=50), " + f"{len(date_states)} date states (expected >=1), " + f"{len(time_states)} time states (expected >=1), " + f"{len(datetime_states)} datetime states (expected >=1). " + f"Total states: {len(states)}" ) # Verify we received a good number of entity states @@ -64,9 +96,65 @@ async def test_host_mode_many_entities( if isinstance(s, SensorState) and isinstance(s.state, float) ] - assert sensor_count >= 50, ( - f"Expected at least 50 sensor states, got {sensor_count}" - ) assert len(sensor_states) >= 50, ( f"Expected at least 50 sensor states, got {len(sensor_states)}" ) + + # Verify we received datetime entity states + date_states = [s for s in states.values() if isinstance(s, DateState)] + time_states = [s for s in states.values() if isinstance(s, TimeState)] + datetime_states = [s for s in states.values() if isinstance(s, DateTimeState)] + + assert len(date_states) >= 1, ( + f"Expected at least 1 date state, got {len(date_states)}" + ) + assert len(time_states) >= 1, ( + f"Expected at least 1 time state, got {len(time_states)}" + ) + assert len(datetime_states) >= 1, ( + f"Expected at least 1 datetime state, got {len(datetime_states)}" + ) + + # Get entity info to verify climate entity details + entities = await client.list_entities_services() + climate_infos = [e for e in entities[0] if isinstance(e, ClimateInfo)] + assert len(climate_infos) >= 1, "Expected at least 1 climate entity" + + climate_info = climate_infos[0] + # Verify the thermostat has presets + assert len(climate_info.supported_presets) > 0, ( + "Expected climate to have presets" + ) + # The thermostat platform uses standard presets (Home, Away, Sleep) + # which should be transmitted properly without string copies + + # Verify specific presets exist + preset_names = [p.name for p in climate_info.supported_presets] + assert "HOME" in preset_names, f"Expected 'HOME' preset, got {preset_names}" + assert "AWAY" in preset_names, f"Expected 'AWAY' preset, got {preset_names}" + assert "SLEEP" in preset_names, f"Expected 'SLEEP' preset, got {preset_names}" + + # Verify datetime entities exist + date_infos = [e for e in entities[0] if isinstance(e, DateInfo)] + time_infos = [e for e in entities[0] if isinstance(e, TimeInfo)] + datetime_infos = [e for e in entities[0] if isinstance(e, DateTimeInfo)] + + assert len(date_infos) >= 1, "Expected at least 1 date entity" + assert len(time_infos) >= 1, "Expected at least 1 time entity" + assert len(datetime_infos) >= 1, "Expected at least 1 datetime entity" + + # Verify the entity names + date_info = date_infos[0] + assert date_info.name == "Test Date", ( + f"Expected date entity name 'Test Date', got {date_info.name}" + ) + + time_info = time_infos[0] + assert time_info.name == "Test Time", ( + f"Expected time entity name 'Test Time', got {time_info.name}" + ) + + datetime_info = datetime_infos[0] + assert datetime_info.name == "Test DateTime", ( + f"Expected datetime entity name 'Test DateTime', got {datetime_info.name}" + ) diff --git a/tests/integration/test_host_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_light_calls.py b/tests/integration/test_light_calls.py index 1c56bbbf9e..1a0a9e553f 100644 --- a/tests/integration/test_light_calls.py +++ b/tests/integration/test_light_calls.py @@ -180,6 +180,69 @@ async def test_light_calls( state = await wait_for_state_change(rgb_light.key) assert state.state is False + # Test color mode combinations to verify get_suitable_color_modes optimization + + # Test 22: White only mode + client.light_command(key=rgbcw_light.key, state=True, white=0.5) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Test 23: Color temperature only mode + client.light_command(key=rgbcw_light.key, state=True, color_temperature=300) + state = await wait_for_state_change(rgbcw_light.key) + assert state.color_temperature == pytest.approx(300) + + # Test 24: Cold/warm white only mode + client.light_command( + key=rgbcw_light.key, state=True, cold_white=0.6, warm_white=0.4 + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.cold_white == pytest.approx(0.6) + assert state.warm_white == pytest.approx(0.4) + + # Test 25: RGB only mode + client.light_command(key=rgb_light.key, state=True, rgb=(0.5, 0.5, 0.5)) + state = await wait_for_state_change(rgb_light.key) + assert state.state is True + + # Test 26: RGB + white combination + client.light_command( + key=rgbcw_light.key, state=True, rgb=(0.3, 0.3, 0.3), white=0.5 + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Test 27: RGB + color temperature combination + client.light_command( + key=rgbcw_light.key, state=True, rgb=(0.4, 0.4, 0.4), color_temperature=280 + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Test 28: RGB + cold/warm white combination + client.light_command( + key=rgbcw_light.key, + state=True, + rgb=(0.2, 0.2, 0.2), + cold_white=0.5, + warm_white=0.5, + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Test 29: White + color temperature combination + client.light_command( + key=rgbcw_light.key, state=True, white=0.6, color_temperature=320 + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + + # Test 30: No specific color parameters (tests default mode selection) + client.light_command(key=rgbcw_light.key, state=True, brightness=0.75) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + assert state.brightness == pytest.approx(0.75) + # Final cleanup - turn all lights off for light in lights: client.light_command( diff --git a/tests/integration/test_loop_disable_enable.py b/tests/integration/test_loop_disable_enable.py index e93fc32178..2a866b1574 100644 --- a/tests/integration/test_loop_disable_enable.py +++ b/tests/integration/test_loop_disable_enable.py @@ -150,7 +150,7 @@ async def test_loop_disable_enable( # Wait for self_disable_10 to disable itself try: await asyncio.wait_for(self_disable_10_disabled.wait(), timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("self_disable_10 did not disable itself within 10 seconds") # Verify it ran at least 10 times before disabling @@ -164,7 +164,7 @@ async def test_loop_disable_enable( # Wait for normal_component to run at least 10 times try: await asyncio.wait_for(normal_component_10_loops.wait(), timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( f"normal_component did not reach 10 loops within timeout, got {len(normal_component_counts)}" ) @@ -172,12 +172,12 @@ async def test_loop_disable_enable( # Wait for redundant operation tests try: await asyncio.wait_for(redundant_enable_tested.wait(), timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("redundant_enable did not test enabling when already enabled") try: await asyncio.wait_for(redundant_disable_tested.wait(), timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail( "redundant_disable did not test disabling when will be disabled" ) @@ -185,7 +185,7 @@ async def test_loop_disable_enable( # Wait to see if self_disable_10 gets re-enabled try: await asyncio.wait_for(self_disable_10_re_enabled.wait(), timeout=5.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("self_disable_10 was not re-enabled within 5 seconds") # Component was re-enabled - verify it ran more times @@ -198,7 +198,7 @@ async def test_loop_disable_enable( # Wait for ISR component to disable itself after 5 loops try: await asyncio.wait_for(isr_component_disabled.wait(), timeout=3.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("ISR component did not disable itself within 3 seconds") # Verify it ran exactly 5 times before disabling @@ -210,7 +210,7 @@ async def test_loop_disable_enable( # Wait for component to be re-enabled by periodic ISR simulation and run again try: await asyncio.wait_for(isr_component_re_enabled.wait(), timeout=2.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("ISR component was not re-enabled after ISR call") # Verify it's running again after ISR enable @@ -222,7 +222,7 @@ async def test_loop_disable_enable( # Wait for pure ISR enable (no main loop enable) to work try: await asyncio.wait_for(isr_component_pure_re_enabled.wait(), timeout=2.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("ISR component was not re-enabled by pure ISR call") # Verify it ran after pure ISR enable @@ -235,7 +235,7 @@ async def test_loop_disable_enable( # Wait for update component to disable its loop try: await asyncio.wait_for(update_component_loop_disabled.wait(), timeout=3.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Update component did not disable its loop within 3 seconds") # Verify it ran exactly 3 loops before disabling @@ -248,7 +248,7 @@ async def test_loop_disable_enable( await asyncio.wait_for( update_component_manual_update_called.wait(), timeout=5.0 ) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Manual component.update was not called within 5 seconds") # The key test: verify that manual component.update worked after loop was disabled diff --git a/tests/integration/test_runtime_stats.py b/tests/integration/test_runtime_stats.py new file mode 100644 index 0000000000..9e93035d83 --- /dev/null +++ b/tests/integration/test_runtime_stats.py @@ -0,0 +1,88 @@ +"""Test runtime statistics component.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_runtime_stats( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test runtime stats logs statistics at configured interval and tracks components.""" + loop = asyncio.get_running_loop() + + # Track how many times we see the total stats + stats_count = 0 + first_stats_future = loop.create_future() + second_stats_future = loop.create_future() + + # Track component stats + component_stats_found = set() + + # Patterns to match - need to handle ANSI color codes and timestamps + # The log format is: [HH:MM:SS][color codes][I][tag]: message + total_stats_pattern = re.compile(r"Total stats \(since boot\):") + # Match component names that may include dots (e.g., template.sensor) + component_pattern = re.compile( + r"^\[[^\]]+\].*?\s+([\w.]+):\s+count=(\d+),\s+avg=([\d.]+)ms" + ) + + def check_output(line: str) -> None: + """Check log output for runtime stats messages.""" + nonlocal stats_count + + # Check for total stats line + if total_stats_pattern.search(line): + stats_count += 1 + + if stats_count == 1 and not first_stats_future.done(): + first_stats_future.set_result(True) + elif stats_count == 2 and not second_stats_future.done(): + second_stats_future.set_result(True) + + # Check for component stats + match = component_pattern.match(line) + if match: + component_name = match.group(1) + component_stats_found.add(component_name) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device is connected + device_info = await client.device_info() + assert device_info is not None + + # Wait for first "Total stats" log (should happen at 1s) + try: + await asyncio.wait_for(first_stats_future, timeout=5.0) + except TimeoutError: + pytest.fail("First 'Total stats' log not seen within 5 seconds") + + # Wait for second "Total stats" log (should happen at 2s) + try: + await asyncio.wait_for(second_stats_future, timeout=5.0) + except TimeoutError: + pytest.fail(f"Second 'Total stats' log not seen. Total seen: {stats_count}") + + # Verify we got at least 2 stats logs + assert stats_count >= 2, ( + f"Expected at least 2 'Total stats' logs, got {stats_count}" + ) + + # Verify we found stats for our components + assert "template.sensor" in component_stats_found, ( + f"Expected template.sensor stats, found: {component_stats_found}" + ) + assert "template.switch" in component_stats_found, ( + f"Expected template.switch stats, found: {component_stats_found}" + ) diff --git a/tests/integration/test_scheduler_bulk_cleanup.py b/tests/integration/test_scheduler_bulk_cleanup.py index 08ff293b84..b52a4a3496 100644 --- a/tests/integration/test_scheduler_bulk_cleanup.py +++ b/tests/integration/test_scheduler_bulk_cleanup.py @@ -103,7 +103,7 @@ async def test_scheduler_bulk_cleanup( # Wait for test completion try: await asyncio.wait_for(test_complete_future, timeout=10.0) - except asyncio.TimeoutError: + except TimeoutError: pytest.fail("Bulk cleanup test timed out") # Verify bulk cleanup was triggered diff --git a/tests/integration/test_scheduler_defer_cancel.py b/tests/integration/test_scheduler_defer_cancel.py index 923cf946c4..34c46bab82 100644 --- a/tests/integration/test_scheduler_defer_cancel.py +++ b/tests/integration/test_scheduler_defer_cancel.py @@ -70,11 +70,13 @@ async def test_scheduler_defer_cancel( test_complete_future.set_result(True) return - if state.key == test_result_entity.key and not test_result_future.done(): - # Event type should be "defer_executed_X" where X is the defer number - if state.event_type.startswith("defer_executed_"): - defer_num = int(state.event_type.split("_")[-1]) - test_result_future.set_result(defer_num) + if ( + state.key == test_result_entity.key + and not test_result_future.done() + and state.event_type.startswith("defer_executed_") + ): + defer_num = int(state.event_type.split("_")[-1]) + test_result_future.set_result(defer_num) client.subscribe_states(on_state) @@ -85,7 +87,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 229b5b98df..2d55b8ae89 100644 --- a/tests/integration/test_scheduler_heap_stress.py +++ b/tests/integration/test_scheduler_heap_stress.py @@ -104,7 +104,7 @@ 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=10.0) - except asyncio.TimeoutError: + except TimeoutError: # Report how many we got missing_ids = sorted(set(range(1000)) - executed_callbacks) pytest.fail( diff --git a/tests/integration/test_scheduler_null_name.py b/tests/integration/test_scheduler_null_name.py index 41bcd8aed7..75864ea2d2 100644 --- a/tests/integration/test_scheduler_null_name.py +++ b/tests/integration/test_scheduler_null_name.py @@ -27,33 +27,33 @@ async def test_scheduler_null_name( 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" + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-null-name" - # List services - _, services = await asyncio.wait_for( - client.list_entities_services(), timeout=5.0 + # 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" ) - - # 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 asyncio.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..1b7da32aaa 100644 --- a/tests/integration/test_scheduler_rapid_cancellation.py +++ b/tests/integration/test_scheduler_rapid_cancellation.py @@ -61,9 +61,10 @@ async def test_scheduler_rapid_cancellation( elif "Total executed:" in line: if match := re.search(r"Total executed: (\d+)", line): test_stats["final_executed"] = int(match.group(1)) - elif "Implicit cancellations (replaced):" in line: - if match := re.search(r"Implicit cancellations \(replaced\): (\d+)", line): - test_stats["final_implicit_cancellations"] = int(match.group(1)) + elif "Implicit cancellations (replaced):" in line and ( + match := re.search(r"Implicit cancellations \(replaced\): (\d+)", line) + ): + test_stats["final_implicit_cancellations"] = int(match.group(1)) # Check for crash indicators if any( @@ -112,7 +113,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..c04b7197c9 --- /dev/null +++ b/tests/integration/test_scheduler_retry_test.py @@ -0,0 +1,296 @@ +"""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() + const_char_done = asyncio.Event() + static_char_done = asyncio.Event() + mixed_cancel_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 + const_char_retry_count = 0 + static_char_retry_count = 0 + + # Track specific test results + cancel_result = None + empty_cancel_result = None + mixed_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, const_char_retry_count, static_char_retry_count + nonlocal cancel_result, empty_cancel_result, mixed_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() + + # Const char retry test + elif "Const char retry" in clean_line: + if match := re.search(r"Const char retry (\d+)", clean_line): + const_char_retry_count = int(match.group(1)) + const_char_done.set() + + # Static const char retry test + elif "Static const char retry" in clean_line: + if match := re.search(r"Static const char retry (\d+)", clean_line): + static_char_retry_count = int(match.group(1)) + static_char_done.set() + + elif "Static cancel result:" in clean_line: + # This is part of test 9, but we don't track it separately + pass + + # Mixed cancel test + elif "Mixed cancel result:" in clean_line: + mixed_cancel_result = "true" in clean_line + mixed_cancel_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 (very wide tolerance for heavy system load) + assert 20 <= backoff_intervals[0] <= 150, ( + f"First interval {backoff_intervals[0]}ms not ~50ms" + ) + # Second interval should be ~100ms (50ms * 2.0) + assert 50 <= backoff_intervals[1] <= 250, ( + f"Second interval {backoff_intervals[1]}ms not ~100ms" + ) + # Third interval should be ~200ms (100ms * 2.0) + assert 100 <= backoff_intervals[2] <= 500, ( + 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=3.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 <= 3, ( + f"Expected 1-3 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 const char retry test + try: + await asyncio.wait_for(const_char_done.wait(), timeout=1.0) + except TimeoutError: + pytest.fail( + f"Const char retry test did not complete. Count: {const_char_retry_count}" + ) + + assert const_char_retry_count == 1, ( + f"Expected 1 const char retry call, got {const_char_retry_count}" + ) + + # Wait for static char retry test + try: + await asyncio.wait_for(static_char_done.wait(), timeout=1.0) + except TimeoutError: + pytest.fail( + f"Static char retry test did not complete. Count: {static_char_retry_count}" + ) + + assert static_char_retry_count == 1, ( + f"Expected 1 static char retry call, got {static_char_retry_count}" + ) + + # Wait for mixed cancel test + try: + await asyncio.wait_for(mixed_cancel_done.wait(), timeout=1.0) + except TimeoutError: + pytest.fail("Mixed cancel test did not complete") + + assert mixed_cancel_result is True, ( + "Mixed string/const char cancel should have succeeded" + ) + + # 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..2f84d11a0d --- /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 + if "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..7200afc2ee --- /dev/null +++ b/tests/script/test_determine_jobs.py @@ -0,0 +1,379 @@ +"""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"]), + 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"], + ), + 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), + patch("subprocess.run") as mock_run, + ): + # Test with hash check returning specific code + 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..63f1f0e600 --- /dev/null +++ b/tests/script/test_helpers.py @@ -0,0 +1,1067 @@ +"""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_large_pr( + monkeypatch: MonkeyPatch, +) -> None: + """Test _get_changed_files_github_actions fallback for PRs with >300 files.""" + monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") + + expected_files = ["file1.py", "file2.cpp"] + + with ( + patch("helpers._get_pr_number_from_github_env", return_value="10214"), + patch("helpers._get_changed_files_from_command") as mock_get, + ): + # First call fails with too many files error, second succeeds with API method + mock_get.side_effect = [ + Exception("Sorry, the diff exceeded the maximum number of files (300)"), + expected_files, + ] + + result = _get_changed_files_github_actions() + + assert mock_get.call_count == 2 + mock_get.assert_any_call(["gh", "pr", "diff", "10214", "--name-only"]) + mock_get.assert_any_call( + [ + "gh", + "api", + "repos/esphome/esphome/pulls/10214/files", + "--paginate", + "--jq", + ".[].filename", + ] + ) + assert result == expected_files + + +def test_get_changed_files_github_actions_pull_request_other_error( + monkeypatch: MonkeyPatch, +) -> None: + """Test _get_changed_files_github_actions re-raises non-file-limit errors.""" + monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") + + with ( + patch("helpers._get_pr_number_from_github_env", return_value="1234"), + patch("helpers._get_changed_files_from_command") as mock_get, + ): + # Error that is not about file limit + mock_get.side_effect = Exception("Command failed: authentication required") + + with pytest.raises(Exception, match="authentication required"): + _get_changed_files_github_actions() + + # Should only be called once (no retry with API) + mock_get.assert_called_once_with(["gh", "pr", "diff", "1234", "--name-only"]) + + +def test_get_changed_files_github_actions_pull_request_no_pr_number( + monkeypatch: MonkeyPatch, +) -> None: + """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" + # 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 4f256ffb33..2157bc20a9 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ICON, + CONF_ID, CONF_INTERNAL, CONF_NAME, ) @@ -510,13 +511,19 @@ 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 + # Check metadata was stored + metadata = CORE.unique_ids[("", "sensor", "temperature")] + assert metadata["name"] == "Temperature" + assert metadata["platform"] == "sensor" # Second entity with different name should pass config2 = {CONF_NAME: "Humidity"} validated2 = validator(config2) assert validated2 == config2 - assert ("sensor", "humidity") in CORE.unique_ids + assert ("", "sensor", "humidity") in CORE.unique_ids + metadata2 = CORE.unique_ids[("", "sensor", "humidity")] + assert metadata2["name"] == "Humidity" # Duplicate entity should fail config3 = {CONF_NAME: "Temperature"} @@ -535,36 +542,28 @@ 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 + metadata1 = CORE.unique_ids[("device1", "sensor", "temperature")] + assert metadata1["device_id"] == "device1" - # 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 + metadata2 = CORE.unique_ids[("device2", "sensor", "temperature")] + assert metadata2["device_id"] == "device2" + + # Duplicate on same device should fail + config3 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} 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 +587,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 ) @@ -607,6 +606,54 @@ def test_entity_different_platforms_yaml_validation( assert result is not None +def test_entity_duplicate_validator_error_message() -> None: + """Test that duplicate entity error messages include helpful metadata.""" + # Create validator for sensor platform + validator = entity_duplicate_validator("sensor") + + # Set current component to simulate validation context for uptime sensor + CORE.current_component = "sensor.uptime" + + # First entity should pass + config1 = {CONF_NAME: "Battery", CONF_ID: ID("battery_1")} + validated1 = validator(config1) + assert validated1 == config1 + + # Reset component to simulate template sensor + CORE.current_component = "sensor.template" + + # Duplicate entity should fail with detailed error + config2 = {CONF_NAME: "Battery", CONF_ID: ID("battery_2")} + with pytest.raises( + Invalid, + match=r"Duplicate sensor entity with name 'Battery' found.*" + r"Conflicts with entity 'Battery' \(id: battery_1\) from component 'sensor\.uptime'", + ): + validator(config2) + + # Clean up + CORE.current_component = None + + +def test_entity_conflict_between_components_yaml( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that conflicts between different components show helpful error messages.""" + result = load_config_from_fixture( + yaml_file, "entity_conflict_components.yaml", FIXTURES_DIR + ) + assert result is None + + # Check for the enhanced error message + captured = capsys.readouterr() + # The error should mention both the conflict and which component created it + assert "Duplicate sensor entity with name 'Battery' found" in captured.out + # Should mention it conflicts with an entity from a specific sensor platform + assert "from component 'sensor." in captured.out + # Should show it's a conflict between wifi_signal and template + assert "sensor.wifi_signal" in captured.out or "sensor.template" in captured.out + + def test_entity_duplicate_validator_internal_entities() -> None: """Test that internal entities are excluded from duplicate name validation.""" # Create validator for sensor platform @@ -616,21 +663,25 @@ def test_entity_duplicate_validator_internal_entities() -> None: config1 = {CONF_NAME: "Temperature"} validated1 = validator(config1) assert validated1 == config1 - assert ("sensor", "temperature") in CORE.unique_ids + # New format includes device_id (empty string for main device) + assert ("", "sensor", "temperature") in CORE.unique_ids # Internal entity with same name should pass (not added to unique_ids) config2 = {CONF_NAME: "Temperature", CONF_INTERNAL: True} validated2 = validator(config2) assert validated2 == config2 # Internal entity should not be added to unique_ids - assert len([k for k in CORE.unique_ids if k == ("sensor", "temperature")]) == 1 + # Count how many times the key appears (should still be 1) + count = sum(1 for k in CORE.unique_ids if k == ("", "sensor", "temperature")) + assert count == 1 # Another internal entity with same name should also pass config3 = {CONF_NAME: "Temperature", CONF_INTERNAL: True} validated3 = validator(config3) assert validated3 == config3 # Still only one entry in unique_ids (from the non-internal entity) - assert len([k for k in CORE.unique_ids if k == ("sensor", "temperature")]) == 1 + count = sum(1 for k in CORE.unique_ids if k == ("", "sensor", "temperature")) + assert count == 1 # Non-internal entity with same name should fail config4 = {CONF_NAME: "Temperature"} diff --git a/tests/unit_tests/fixtures/core/entity_helpers/entity_conflict_components.yaml b/tests/unit_tests/fixtures/core/entity_helpers/entity_conflict_components.yaml new file mode 100644 index 0000000000..6a1df0f7b4 --- /dev/null +++ b/tests/unit_tests/fixtures/core/entity_helpers/entity_conflict_components.yaml @@ -0,0 +1,20 @@ +esphome: + name: test-device + +esp32: + board: esp32dev + +# Uptime sensor +sensor: + - platform: uptime + name: "Battery" + id: uptime_battery + +# Template sensor also named "Battery" - this should conflict + - platform: template + name: "Battery" + id: template_battery + lambda: |- + return 95.0; + unit_of_measurement: "%" + update_interval: 60s diff --git a/tests/unit_tests/fixtures/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/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_log.py b/tests/unit_tests/test_log.py new file mode 100644 index 0000000000..02798f1029 --- /dev/null +++ b/tests/unit_tests/test_log.py @@ -0,0 +1,80 @@ +import pytest + +from esphome.log import AnsiFore, AnsiStyle, color + + +def test_color_keep_returns_unchanged_message() -> None: + """Test that AnsiFore.KEEP returns the message unchanged.""" + msg = "test message" + result = color(AnsiFore.KEEP, msg) + assert result == msg + + +def test_color_keep_ignores_reset_parameter() -> None: + """Test that reset parameter is ignored when using AnsiFore.KEEP.""" + msg = "test message" + result_with_reset = color(AnsiFore.KEEP, msg, reset=True) + result_without_reset = color(AnsiFore.KEEP, msg, reset=False) + assert result_with_reset == msg + assert result_without_reset == msg + + +def test_color_applies_color_code() -> None: + """Test that color codes are properly applied to messages.""" + msg = "test message" + result = color(AnsiFore.RED, msg, reset=False) + assert result == f"{AnsiFore.RED.value}{msg}" + + +def test_color_applies_reset_when_requested() -> None: + """Test that RESET_ALL is added when reset=True.""" + msg = "test message" + result = color(AnsiFore.GREEN, msg, reset=True) + expected = f"{AnsiFore.GREEN.value}{msg}{AnsiStyle.RESET_ALL.value}" + assert result == expected + + +def test_color_no_reset_when_not_requested() -> None: + """Test that RESET_ALL is not added when reset=False.""" + msg = "test message" + result = color(AnsiFore.BLUE, msg, reset=False) + expected = f"{AnsiFore.BLUE.value}{msg}" + assert result == expected + + +def test_color_with_empty_message() -> None: + """Test color function with empty message.""" + result = color(AnsiFore.YELLOW, "", reset=True) + expected = f"{AnsiFore.YELLOW.value}{AnsiStyle.RESET_ALL.value}" + assert result == expected + + +@pytest.mark.parametrize( + "col", + [ + AnsiFore.BLACK, + AnsiFore.RED, + AnsiFore.GREEN, + AnsiFore.YELLOW, + AnsiFore.BLUE, + AnsiFore.MAGENTA, + AnsiFore.CYAN, + AnsiFore.WHITE, + AnsiFore.RESET, + ], +) +def test_all_ansi_colors(col: AnsiFore) -> None: + """Test that all AnsiFore colors work correctly.""" + msg = "test" + result = color(col, msg, reset=True) + expected = f"{col.value}{msg}{AnsiStyle.RESET_ALL.value}" + assert result == expected + + +def test_ansi_fore_keep_is_enum_member() -> None: + """Ensure AnsiFore.KEEP is an Enum member and evaluates to truthy.""" + assert isinstance(AnsiFore.KEEP, AnsiFore) + # Enum members are truthy, even with empty string values + assert bool(AnsiFore.KEEP) is True + # But the value itself is still an empty string + assert AnsiFore.KEEP.value == "" diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index b377499d29..b2b7cb1ea4 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -18,11 +18,10 @@ def sort_dicts(obj): """Recursively sort dictionaries for order-insensitive comparison.""" if isinstance(obj, dict): return {k: sort_dicts(obj[k]) for k in sorted(obj)} - elif isinstance(obj, list): + if isinstance(obj, list): # Lists are not sorted; we preserve order return [sort_dicts(i) for i in obj] - else: - return obj + return obj def dict_diff(a, b, path=""): @@ -31,10 +30,8 @@ def dict_diff(a, b, path=""): if isinstance(a, dict) and isinstance(b, dict): a_keys = set(a) b_keys = set(b) - for key in a_keys - b_keys: - diffs.append(f"{path}/{key} only in actual") - for key in b_keys - a_keys: - diffs.append(f"{path}/{key} only in expected") + diffs.extend(f"{path}/{key} only in actual" for key in a_keys - b_keys) + diffs.extend(f"{path}/{key} only in expected" for key in b_keys - a_keys) for key in a_keys & b_keys: diffs.extend(dict_diff(a[key], b[key], f"{path}/{key}")) elif isinstance(a, list) and isinstance(b, list): @@ -42,14 +39,16 @@ def dict_diff(a, b, path=""): for i in range(min_len): diffs.extend(dict_diff(a[i], b[i], f"{path}[{i}]")) if len(a) > len(b): - for i in range(min_len, len(a)): - diffs.append(f"{path}[{i}] only in actual: {a[i]!r}") + diffs.extend( + f"{path}[{i}] only in actual: {a[i]!r}" for i in range(min_len, len(a)) + ) 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}") + diffs.extend( + f"{path}[{i}] only in expected: {b[i]!r}" + for i in range(min_len, len(b)) + ) + elif a != b: + diffs.append(f"\t{path}: actual={a!r} expected={b!r}") return diffs diff --git a/tests/unit_tests/test_vscode.py b/tests/unit_tests/test_vscode.py index 6e0bde23b2..4b28a2215b 100644 --- a/tests/unit_tests/test_vscode.py +++ b/tests/unit_tests/test_vscode.py @@ -22,8 +22,7 @@ def _run_repl_test(input_data): call[0][0] for call in mock_stdout.write.call_args_list ).strip() splitted_output = full_output.split("\n") - remove_version = splitted_output[1:] # remove first entry with version info - return remove_version + return splitted_output[1:] # remove first entry with version info def _validate(file_path: str):