mirror of
https://github.com/esphome/esphome.git
synced 2025-11-02 08:01:50 +00:00
Compare commits
3 Commits
jesserockz
...
jesserockz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
140792d8a9 | ||
|
|
14e6918eb1 | ||
|
|
a794a8f3e4 |
@@ -1,222 +0,0 @@
|
||||
# ESPHome AI Collaboration Guide
|
||||
|
||||
This document provides essential context for AI models interacting with this project. Adhering to these guidelines will ensure consistency and maintain code quality.
|
||||
|
||||
## 1. Project Overview & Purpose
|
||||
|
||||
* **Primary Goal:** ESPHome is a system to configure microcontrollers (like ESP32, ESP8266, RP2040, and LibreTiny-based chips) using simple yet powerful YAML configuration files. It generates C++ firmware that can be compiled and flashed to these devices, allowing users to control them remotely through home automation systems.
|
||||
* **Business Domain:** Internet of Things (IoT), Home Automation.
|
||||
|
||||
## 2. Core Technologies & Stack
|
||||
|
||||
* **Languages:** Python (>=3.10), C++ (gnu++20)
|
||||
* **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF.
|
||||
* **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative.
|
||||
* **Configuration:** YAML.
|
||||
* **Key Libraries/Dependencies:**
|
||||
* **Python:** `voluptuous` (for configuration validation), `PyYAML` (for parsing configuration files), `paho-mqtt` (for MQTT communication), `tornado` (for the web server), `aioesphomeapi` (for the native API).
|
||||
* **C++:** `ArduinoJson` (for JSON serialization/deserialization), `AsyncMqttClient-esphome` (for MQTT), `ESPAsyncWebServer` (for the web server).
|
||||
* **Package Manager(s):** `pip` (for Python dependencies), `platformio` (for C++/PlatformIO dependencies).
|
||||
* **Communication Protocols:** Protobuf (for native API), MQTT, HTTP.
|
||||
|
||||
## 3. Architectural Patterns
|
||||
|
||||
* **Overall Architecture:** The project follows a code-generation architecture. The Python code parses user-defined YAML configuration files and generates C++ source code. This C++ code is then compiled and flashed to the target microcontroller using PlatformIO.
|
||||
|
||||
* **Directory Structure Philosophy:**
|
||||
* `/esphome`: Contains the core Python source code for the ESPHome application.
|
||||
* `/esphome/components`: Contains the individual components that can be used in ESPHome configurations. Each component is a self-contained unit with its own C++ and Python code.
|
||||
* `/tests`: Contains all unit and integration tests for the Python code.
|
||||
* `/docker`: Contains Docker-related files for building and running ESPHome in a container.
|
||||
* `/script`: Contains helper scripts for development and maintenance.
|
||||
|
||||
* **Core Architectural Components:**
|
||||
1. **Configuration System** (`esphome/config*.py`): Handles YAML parsing and validation using Voluptuous, schema definitions, and multi-platform configurations.
|
||||
2. **Code Generation** (`esphome/codegen.py`, `esphome/cpp_generator.py`): Manages Python to C++ code generation, template processing, and build flag management.
|
||||
3. **Component System** (`esphome/components/`): Contains modular hardware and software components with platform-specific implementations and dependency management.
|
||||
4. **Core Framework** (`esphome/core/`): Manages the application lifecycle, hardware abstraction, and component registration.
|
||||
5. **Dashboard** (`esphome/dashboard/`): A web-based interface for device configuration, management, and OTA updates.
|
||||
|
||||
* **Platform Support:**
|
||||
1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (S2, S3, C3, etc.) and both IDF and Arduino frameworks.
|
||||
2. **ESP8266** (`components/esp8266/`): Espressif ESP8266. Arduino framework only, with memory constraints.
|
||||
3. **RP2040** (`components/rp2040/`): Raspberry Pi Pico/RP2040. Arduino framework with PIO (Programmable I/O) support.
|
||||
4. **LibreTiny** (`components/libretiny/`): Realtek and Beken chips. Supports multiple chip families and auto-generated components.
|
||||
|
||||
## 4. Coding Conventions & Style Guide
|
||||
|
||||
* **Formatting:**
|
||||
* **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`.
|
||||
* **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`.
|
||||
|
||||
* **Naming Conventions:**
|
||||
* **Python:** Follows PEP 8. Use clear, descriptive names following snake_case.
|
||||
* **C++:** Follows the Google C++ Style Guide.
|
||||
|
||||
* **Component Structure:**
|
||||
* **Standard Files:**
|
||||
```
|
||||
components/[component_name]/
|
||||
├── __init__.py # Component configuration schema and code generation
|
||||
├── [component].h # C++ header file (if needed)
|
||||
├── [component].cpp # C++ implementation (if needed)
|
||||
└── [platform]/ # Platform-specific implementations
|
||||
├── __init__.py # Platform-specific configuration
|
||||
├── [platform].h # Platform C++ header
|
||||
└── [platform].cpp # Platform C++ implementation
|
||||
```
|
||||
|
||||
* **Component Metadata:**
|
||||
- `DEPENDENCIES`: List of required components
|
||||
- `AUTO_LOAD`: Components to automatically load
|
||||
- `CONFLICTS_WITH`: Incompatible components
|
||||
- `CODEOWNERS`: GitHub usernames responsible for maintenance
|
||||
- `MULTI_CONF`: Whether multiple instances are allowed
|
||||
|
||||
* **Code Generation & Common Patterns:**
|
||||
* **Configuration Schema Pattern:**
|
||||
```python
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_KEY, CONF_ID
|
||||
|
||||
CONF_PARAM = "param" # A constant that does not yet exist in esphome/const.py
|
||||
|
||||
my_component_ns = cg.esphome_ns.namespace("my_component")
|
||||
MyComponent = my_component_ns.class_("MyComponent", cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema({
|
||||
cv.GenerateID(): cv.declare_id(MyComponent),
|
||||
cv.Required(CONF_KEY): cv.string,
|
||||
cv.Optional(CONF_PARAM, default=42): cv.int_,
|
||||
}).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_key(config[CONF_KEY]))
|
||||
cg.add(var.set_param(config[CONF_PARAM]))
|
||||
```
|
||||
|
||||
* **C++ Class Pattern:**
|
||||
```cpp
|
||||
namespace esphome {
|
||||
namespace my_component {
|
||||
|
||||
class MyComponent : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_key(const std::string &key) { this->key_ = key; }
|
||||
void set_param(int param) { this->param_ = param; }
|
||||
|
||||
protected:
|
||||
std::string key_;
|
||||
int param_{0};
|
||||
};
|
||||
|
||||
} // namespace my_component
|
||||
} // namespace esphome
|
||||
```
|
||||
|
||||
* **Common Component Examples:**
|
||||
- **Sensor:**
|
||||
```python
|
||||
from esphome.components import sensor
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend(cv.polling_component_schema("60s"))
|
||||
async def to_code(config):
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
```
|
||||
|
||||
- **Binary Sensor:**
|
||||
```python
|
||||
from esphome.components import binary_sensor
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({ ... })
|
||||
async def to_code(config):
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
```
|
||||
|
||||
- **Switch:**
|
||||
```python
|
||||
from esphome.components import switch
|
||||
CONFIG_SCHEMA = switch.switch_schema().extend({ ... })
|
||||
async def to_code(config):
|
||||
var = await switch.new_switch(config)
|
||||
```
|
||||
|
||||
* **Configuration Validation:**
|
||||
* **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`.
|
||||
* **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
|
||||
* **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `cv.only_with_arduino`.
|
||||
* **Schema Extensions:**
|
||||
```python
|
||||
CONFIG_SCHEMA = cv.Schema({ ... })
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x48))
|
||||
.extend(spi.spi_device_schema(cs_pin_required=True))
|
||||
```
|
||||
|
||||
## 5. Key Files & Entrypoints
|
||||
|
||||
* **Main Entrypoint(s):** `esphome/__main__.py` is the main entrypoint for the ESPHome command-line interface.
|
||||
* **Configuration:**
|
||||
* `pyproject.toml`: Defines the Python project metadata and dependencies.
|
||||
* `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers.
|
||||
* `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting.
|
||||
* **CI/CD Pipeline:** Defined in `.github/workflows`.
|
||||
|
||||
## 6. Development & Testing Workflow
|
||||
|
||||
* **Local Development Environment:** Use the provided Docker container or create a Python virtual environment and install dependencies from `requirements_dev.txt`.
|
||||
* **Running Commands:** Use the `script/run-in-env.py` script to execute commands within the project's virtual environment. For example, to run the linter: `python3 script/run-in-env.py pre-commit run`.
|
||||
* **Testing:**
|
||||
* **Python:** Run unit tests with `pytest`.
|
||||
* **C++:** Use `clang-tidy` for static analysis.
|
||||
* **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
|
||||
```
|
||||
tests/
|
||||
├── test_build_components/ # Base test configurations
|
||||
└── components/[component]/ # Component-specific tests
|
||||
```
|
||||
Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
|
||||
* **Debugging and Troubleshooting:**
|
||||
* **Debug Tools:**
|
||||
- `esphome config <file>.yaml` to validate configuration.
|
||||
- `esphome compile <file>.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.
|
||||
@@ -1 +0,0 @@
|
||||
0c2acbc16bfb7d63571dbe7042f94f683be25e4ca8a0f158a960a94adac4b931
|
||||
@@ -1,4 +1,4 @@
|
||||
[run]
|
||||
omit =
|
||||
omit =
|
||||
esphome/components/*
|
||||
tests/integration/*
|
||||
|
||||
96
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
96
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,96 +0,0 @@
|
||||
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.
|
||||
|
||||
⚠️ **WARNING: Do NOT paste logs, stack traces, or error messages here!**
|
||||
Use the "Logs" section below instead. Issues with logs
|
||||
in this field will be automatically closed.
|
||||
|
||||
- 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: Logs
|
||||
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.
|
||||
26
.github/ISSUE_TEMPLATE/config.yml
vendored
26
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,21 +1,15 @@
|
||||
---
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- 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: 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: 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.
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -26,7 +26,6 @@
|
||||
- [ ] RP2040
|
||||
- [ ] BK72xx
|
||||
- [ ] RTL87xx
|
||||
- [ ] nRF52840
|
||||
|
||||
## Example entry for `config.yaml`:
|
||||
|
||||
|
||||
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -41,7 +41,7 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
python -m venv venv
|
||||
source ./venv/Scripts/activate
|
||||
./venv/Scripts/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_test.txt
|
||||
pip install -e .
|
||||
|
||||
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
@@ -1 +0,0 @@
|
||||
../.ai/instructions.md
|
||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -9,9 +9,6 @@ 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
|
||||
@@ -23,17 +20,11 @@ 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
|
||||
|
||||
248
.github/workflows/auto-close-logs-in-problem.yml
vendored
248
.github/workflows/auto-close-logs-in-problem.yml
vendored
@@ -1,248 +0,0 @@
|
||||
name: Auto-close issues with logs in problem field
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'Issue number to check for logs'
|
||||
required: true
|
||||
type: number
|
||||
|
||||
jobs:
|
||||
check-logs-in-problem:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.issue.state == 'open' || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@esphomebot reopen')) || github.event_name == 'workflow_dispatch'
|
||||
steps:
|
||||
- name: Check for logs and handle issue state
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
// Handle different trigger types
|
||||
let issue, isReassessment;
|
||||
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
// Manual dispatch - get issue from input
|
||||
const issueNumber = ${{ github.event.inputs.issue_number }};
|
||||
console.log('Manual dispatch for issue:', issueNumber);
|
||||
|
||||
const issueResponse = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(issueNumber)
|
||||
});
|
||||
|
||||
issue = issueResponse.data;
|
||||
isReassessment = false; // Treat manual dispatch as initial check
|
||||
} else {
|
||||
// Normal event-driven flow
|
||||
issue = context.payload.issue;
|
||||
isReassessment = context.eventName === 'issue_comment' && context.payload.comment.body.includes('@esphomebot reopen');
|
||||
}
|
||||
|
||||
console.log('Event type:', context.eventName);
|
||||
console.log('Is reassessment:', isReassessment);
|
||||
console.log('Issue state:', issue.state);
|
||||
|
||||
// Extract the problem section from the issue body
|
||||
const body = issue.body || '';
|
||||
|
||||
// Look for the problem section between "### The problem" and the next section
|
||||
const problemMatch = body.match(/### The problem\s*\n([\s\S]*?)(?=\n### |$)/i);
|
||||
|
||||
if (!problemMatch) {
|
||||
console.log('Could not find problem section');
|
||||
if (isReassessment) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: '❌ Could not find the "The problem" section in the issue template. Please make sure you are using the proper issue template format.'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const problemText = problemMatch[1].trim();
|
||||
console.log('Problem text length:', problemText.length);
|
||||
|
||||
// Function to check if text contains logs
|
||||
function checkForLogs(text) {
|
||||
// Patterns that indicate logs/stack traces/error messages
|
||||
const logPatterns = [
|
||||
// ESPHome specific log patterns with brackets
|
||||
/^\[[DIWEVC]\]\[[^\]]+(?::\d+)?\]:/m, // [D][component:123]: message
|
||||
/^\[\d{2}:\d{2}:\d{2}\]\[[DIWEVC]\]\[[^\]]+(?::\d+)?\]:/m, // [12:34:56][D][component:123]: message
|
||||
/^\[\d{2}:\d{2}:\d{2}\.\d{3}\]\[[DIWEVC]\]\[[^\]]+(?::\d+)?\]:/m, // [12:34:56.123][D][component:123]: message
|
||||
|
||||
// Common log prefixes
|
||||
/^\[[\d\s\-:\.]+\]/m, // [timestamp] format
|
||||
/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/m, // YYYY-MM-DD HH:MM:SS
|
||||
/^\w+\s+\d{2}:\d{2}:\d{2}/m, // INFO 12:34:56
|
||||
|
||||
// Error indicators
|
||||
/^(ERROR|WARN|WARNING|FATAL|DEBUG|INFO|TRACE)[\s:]/mi,
|
||||
/^(Exception|Error|Traceback|Stack trace)/mi,
|
||||
/at\s+[\w\.]+\([^)]*:\d+:\d+\)/m, // Stack trace format
|
||||
/^\s*File\s+"[^"]*",\s+line\s+\d+/m, // Python traceback
|
||||
|
||||
// Legacy ESPHome log patterns
|
||||
/^\[\d{2}:\d{2}:\d{2}\]\[/m, // [12:34:56][component]
|
||||
/^WARNING\s+[^:\s]+:/m, // WARNING component:
|
||||
/^ERROR\s+[^:\s]+:/m, // ERROR component:
|
||||
|
||||
// Multiple consecutive lines starting with similar patterns
|
||||
/(^(INFO|DEBUG|WARN|ERROR)[^\n]*\n){3,}/mi,
|
||||
/(^\[[DIWEVC]\]\[[^\]]+\][^\n]*\n){3,}/mi, // Multiple ESPHome log lines
|
||||
|
||||
// Hex dumps or binary data
|
||||
/0x[0-9a-f]{4,}/i,
|
||||
/[0-9a-f]{8,}/,
|
||||
|
||||
// Compilation errors
|
||||
/error:\s+/i,
|
||||
/:\d+:\d+:\s+(error|warning):/i,
|
||||
|
||||
// Very long lines (often log output)
|
||||
/.{200,}/
|
||||
];
|
||||
|
||||
const hasLogs = logPatterns.some(pattern => {
|
||||
const matches = pattern.test(text);
|
||||
if (matches) {
|
||||
console.log('Pattern matched:', pattern.toString());
|
||||
}
|
||||
return matches;
|
||||
});
|
||||
|
||||
// Additional heuristics
|
||||
const lineCount = text.split('\n').length;
|
||||
const hasLotsOfLines = lineCount > 20; // More than 20 lines might be logs
|
||||
|
||||
const hasCodeBlocks = (text.match(/```/g) || []).length >= 2;
|
||||
const longCodeBlock = hasCodeBlocks && text.length > 1000;
|
||||
|
||||
console.log(`Lines: ${lineCount}, Has logs: ${hasLogs}, Long code block: ${longCodeBlock}`);
|
||||
|
||||
return hasLogs || (hasLotsOfLines && longCodeBlock);
|
||||
}
|
||||
|
||||
const hasLogsInProblem = checkForLogs(problemText);
|
||||
|
||||
// Handle reassessment (when @esphomebot is mentioned)
|
||||
if (isReassessment) {
|
||||
if (!hasLogsInProblem) {
|
||||
// No logs found, check if issue was auto-closed and reopen it
|
||||
if (issue.state === 'closed') {
|
||||
// Check if it has the auto-closed label
|
||||
const labels = issue.labels.map(label => label.name);
|
||||
if (labels.includes('auto-closed')) {
|
||||
console.log('Reopening issue - logs have been moved');
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
// Remove auto-closed and invalid labels
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
name: 'auto-closed'
|
||||
});
|
||||
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
name: 'invalid'
|
||||
});
|
||||
|
||||
// Find and edit the original auto-close comment
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number
|
||||
});
|
||||
|
||||
const autoCloseComment = comments.data.find(comment =>
|
||||
comment.user.login === 'github-actions[bot]' &&
|
||||
comment.body.includes('automatically closed because it appears to contain logs')
|
||||
);
|
||||
|
||||
if (autoCloseComment) {
|
||||
const updatedComment = `✅ **ISSUE REOPENED**
|
||||
|
||||
Thank you for helping us maintain organized issue reports! 🙏`;
|
||||
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: autoCloseComment.id,
|
||||
body: updatedComment
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: '❌ Logs are still detected in the "The problem" section. Please move them to the "Logs" section and try again.'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle initial issue opening
|
||||
if (!hasLogsInProblem) {
|
||||
console.log('No logs detected in problem field');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Logs detected in problem field, closing issue');
|
||||
|
||||
// Close the issue
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
// Add a comment explaining why it was closed
|
||||
const comment = `This issue has been automatically closed because it appears to contain logs, stack traces, or error messages in the "The problem" field.
|
||||
|
||||
⚠️ **Please follow the issue template correctly:**
|
||||
- Use the "The problem" field to **describe** your issue in plain English
|
||||
- Put logs, error messages, and stack traces in the "Logs" section instead
|
||||
|
||||
To reopen this issue:
|
||||
1. Edit your original issue description
|
||||
2. Move any logs/error messages to the appropriate "Logs" section
|
||||
3. Rewrite the "The problem" section with a clear description of what you were trying to do and what went wrong
|
||||
4. Comment exactly \`@esphomebot reopen\` to reassess and automatically reopen if fixed
|
||||
|
||||
Thank you for helping us maintain organized issue reports! 🙏`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
// Add labels
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: ['invalid', 'auto-closed']
|
||||
});
|
||||
449
.github/workflows/auto-label-pr.yml
vendored
449
.github/workflows/auto-label-pr.yml
vendored
@@ -1,449 +0,0 @@
|
||||
name: Auto Label PR
|
||||
|
||||
on:
|
||||
# Runs only on pull_request_target due to having access to a App token.
|
||||
# This means PRs from forks will not be able to alter this workflow to get the tokens
|
||||
pull_request_target:
|
||||
types: [labeled, opened, reopened, synchronize, edited]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TARGET_PLATFORMS: |
|
||||
esp32
|
||||
esp8266
|
||||
rp2040
|
||||
libretiny
|
||||
bk72xx
|
||||
rtl87xx
|
||||
ln882x
|
||||
nrf52
|
||||
host
|
||||
PLATFORM_COMPONENTS: |
|
||||
alarm_control_panel
|
||||
audio_adc
|
||||
audio_dac
|
||||
binary_sensor
|
||||
button
|
||||
canbus
|
||||
climate
|
||||
cover
|
||||
datetime
|
||||
display
|
||||
event
|
||||
fan
|
||||
light
|
||||
lock
|
||||
media_player
|
||||
microphone
|
||||
number
|
||||
one_wire
|
||||
ota
|
||||
output
|
||||
packet_transport
|
||||
select
|
||||
sensor
|
||||
speaker
|
||||
stepper
|
||||
switch
|
||||
text
|
||||
text_sensor
|
||||
time
|
||||
touchscreen
|
||||
update
|
||||
valve
|
||||
SMALL_PR_THRESHOLD: 30
|
||||
MAX_LABELS: 15
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Get changes
|
||||
id: changes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Get PR number
|
||||
pr_number="${{ github.event.pull_request.number }}"
|
||||
|
||||
# Get list of changed files using gh CLI
|
||||
files=$(gh pr diff $pr_number --name-only)
|
||||
echo "files<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$files" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# Get file stats (additions + deletions) using gh CLI
|
||||
stats=$(gh pr view $pr_number --json files --jq '.files | map(.additions + .deletions) | add')
|
||||
echo "total_changes=${stats:-0}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Auto Label PR
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.issue.number;
|
||||
|
||||
// Get current labels
|
||||
const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number
|
||||
});
|
||||
const currentLabels = currentLabelsData.map(label => label.name);
|
||||
|
||||
// Define managed labels that this workflow controls
|
||||
const managedLabels = currentLabels.filter(label =>
|
||||
label.startsWith('component: ') ||
|
||||
[
|
||||
'new-component',
|
||||
'new-platform',
|
||||
'new-target-platform',
|
||||
'merging-to-release',
|
||||
'merging-to-beta',
|
||||
'core',
|
||||
'small-pr',
|
||||
'dashboard',
|
||||
'github-actions',
|
||||
'by-code-owner',
|
||||
'has-tests',
|
||||
'needs-tests',
|
||||
'needs-docs',
|
||||
'too-big',
|
||||
'labeller-recheck'
|
||||
].includes(label)
|
||||
);
|
||||
|
||||
console.log('Current labels:', currentLabels.join(', '));
|
||||
console.log('Managed labels:', managedLabels.join(', '));
|
||||
|
||||
// Get changed files
|
||||
const changedFiles = `${{ steps.changes.outputs.files }}`.split('\n').filter(f => f.length > 0);
|
||||
const totalChanges = parseInt('${{ steps.changes.outputs.total_changes }}') || 0;
|
||||
|
||||
console.log('Changed files:', changedFiles.length);
|
||||
console.log('Total changes:', totalChanges);
|
||||
|
||||
const labels = new Set();
|
||||
|
||||
// Get environment variables
|
||||
const targetPlatforms = `${{ env.TARGET_PLATFORMS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim());
|
||||
const platformComponents = `${{ env.PLATFORM_COMPONENTS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim());
|
||||
const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}');
|
||||
const maxLabels = parseInt('${{ env.MAX_LABELS }}');
|
||||
|
||||
// Strategy: Merge to release or beta branch
|
||||
const baseRef = context.payload.pull_request.base.ref;
|
||||
if (baseRef !== 'dev') {
|
||||
if (baseRef === 'release') {
|
||||
labels.add('merging-to-release');
|
||||
} else if (baseRef === 'beta') {
|
||||
labels.add('merging-to-beta');
|
||||
}
|
||||
|
||||
// When targeting non-dev branches, only use merge warning labels
|
||||
const finalLabels = Array.from(labels);
|
||||
console.log('Computed labels (merge branch only):', finalLabels.join(', '));
|
||||
|
||||
// Add new labels
|
||||
if (finalLabels.length > 0) {
|
||||
console.log(`Adding labels: ${finalLabels.join(', ')}`);
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
labels: finalLabels
|
||||
});
|
||||
}
|
||||
|
||||
// Remove old managed labels that are no longer needed
|
||||
const labelsToRemove = managedLabels.filter(label =>
|
||||
!finalLabels.includes(label)
|
||||
);
|
||||
|
||||
for (const label of labelsToRemove) {
|
||||
console.log(`Removing label: ${label}`);
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
name: label
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`Failed to remove label ${label}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return; // Exit early, don't process other strategies
|
||||
}
|
||||
|
||||
// Strategy: Component and Platform labeling
|
||||
const componentRegex = /^esphome\/components\/([^\/]+)\//;
|
||||
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${targetPlatforms.join('|')})/`);
|
||||
|
||||
for (const file of changedFiles) {
|
||||
// Check for component changes
|
||||
const componentMatch = file.match(componentRegex);
|
||||
if (componentMatch) {
|
||||
const component = componentMatch[1];
|
||||
labels.add(`component: ${component}`);
|
||||
}
|
||||
|
||||
// Check for target platform changes
|
||||
const platformMatch = file.match(targetPlatformRegex);
|
||||
if (platformMatch) {
|
||||
const targetPlatform = platformMatch[1];
|
||||
labels.add(`platform: ${targetPlatform}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get PR files for new component/platform detection
|
||||
const { data: prFiles } = await github.rest.pulls.listFiles({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
|
||||
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
||||
|
||||
// Strategy: New Component detection
|
||||
for (const file of addedFiles) {
|
||||
// Check for new component files: esphome/components/{component}/__init__.py
|
||||
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
|
||||
if (componentMatch) {
|
||||
try {
|
||||
// Read the content directly from the filesystem since we have it checked out
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
|
||||
// Strategy: New Target Platform detection
|
||||
if (content.includes('IS_TARGET_PLATFORM = True')) {
|
||||
labels.add('new-target-platform');
|
||||
}
|
||||
labels.add('new-component');
|
||||
} catch (error) {
|
||||
console.log(`Failed to read content of ${file}:`, error.message);
|
||||
// Fallback: assume it's a new component if we can't read the content
|
||||
labels.add('new-component');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy: New Platform detection
|
||||
for (const file of addedFiles) {
|
||||
// Check for new platform files: esphome/components/{component}/{platform}.py
|
||||
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
|
||||
if (platformFileMatch) {
|
||||
const [, component, platform] = platformFileMatch;
|
||||
if (platformComponents.includes(platform)) {
|
||||
labels.add('new-platform');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for new platform files: esphome/components/{component}/{platform}/__init__.py
|
||||
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
|
||||
if (platformDirMatch) {
|
||||
const [, component, platform] = platformDirMatch;
|
||||
if (platformComponents.includes(platform)) {
|
||||
labels.add('new-platform');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const coreFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/core/') ||
|
||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||
);
|
||||
|
||||
if (coreFiles.length > 0) {
|
||||
labels.add('core');
|
||||
}
|
||||
|
||||
// Strategy: Small PR detection
|
||||
if (totalChanges <= smallPrThreshold) {
|
||||
labels.add('small-pr');
|
||||
}
|
||||
|
||||
// Strategy: Dashboard changes
|
||||
const dashboardFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/dashboard/') ||
|
||||
file.startsWith('esphome/components/dashboard_import/')
|
||||
);
|
||||
|
||||
if (dashboardFiles.length > 0) {
|
||||
labels.add('dashboard');
|
||||
}
|
||||
|
||||
// Strategy: GitHub Actions changes
|
||||
const githubActionsFiles = changedFiles.filter(file =>
|
||||
file.startsWith('.github/workflows/')
|
||||
);
|
||||
|
||||
if (githubActionsFiles.length > 0) {
|
||||
labels.add('github-actions');
|
||||
}
|
||||
|
||||
// Strategy: Code Owner detection
|
||||
try {
|
||||
// Fetch CODEOWNERS file from the repository (in case it was changed in this PR)
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'CODEOWNERS',
|
||||
});
|
||||
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
// Parse CODEOWNERS file
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
let isCodeOwner = false;
|
||||
|
||||
// Precompile CODEOWNERS patterns into regex objects
|
||||
const codeownersRegexes = codeownersLines.map(line => {
|
||||
const parts = line.split(/\s+/);
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
let regex;
|
||||
if (pattern.endsWith('*')) {
|
||||
// Directory pattern like "esphome/components/api/*"
|
||||
const dir = pattern.slice(0, -1);
|
||||
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||
} else if (pattern.includes('*')) {
|
||||
// Glob pattern
|
||||
const regexPattern = pattern
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\\*/g, '.*');
|
||||
regex = new RegExp(`^${regexPattern}$`);
|
||||
} else {
|
||||
// Exact match
|
||||
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||
}
|
||||
|
||||
return { regex, owners };
|
||||
});
|
||||
|
||||
for (const file of changedFiles) {
|
||||
for (const { regex, owners } of codeownersRegexes) {
|
||||
if (regex.test(file)) {
|
||||
// Check if PR author is in the owners list
|
||||
if (owners.some(owner => owner === `@${prAuthor}`)) {
|
||||
isCodeOwner = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isCodeOwner) break;
|
||||
}
|
||||
|
||||
if (isCodeOwner) {
|
||||
labels.add('by-code-owner');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to read or parse CODEOWNERS file:', error.message);
|
||||
}
|
||||
|
||||
// Strategy: Test detection
|
||||
const testFiles = changedFiles.filter(file =>
|
||||
file.startsWith('tests/')
|
||||
);
|
||||
|
||||
if (testFiles.length > 0) {
|
||||
labels.add('has-tests');
|
||||
} else {
|
||||
// Only check for needs-tests if this is a new component or new platform
|
||||
if (labels.has('new-component') || labels.has('new-platform')) {
|
||||
labels.add('needs-tests');
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy: Documentation check for new components/platforms
|
||||
if (labels.has('new-component') || labels.has('new-platform')) {
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
|
||||
// Look for documentation PR links
|
||||
// Patterns to match:
|
||||
// - https://github.com/esphome/esphome-docs/pull/1234
|
||||
// - esphome/esphome-docs#1234
|
||||
const docsPrPatterns = [
|
||||
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
|
||||
/esphome\/esphome-docs#\d+/
|
||||
];
|
||||
|
||||
const hasDocsLink = docsPrPatterns.some(pattern => pattern.test(prBody));
|
||||
|
||||
if (!hasDocsLink) {
|
||||
labels.add('needs-docs');
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Set to Array
|
||||
let finalLabels = Array.from(labels);
|
||||
|
||||
console.log('Computed labels:', finalLabels.join(', '));
|
||||
|
||||
// Don't set more than max labels
|
||||
if (finalLabels.length > maxLabels) {
|
||||
const originalLength = finalLabels.length;
|
||||
console.log(`Not setting ${originalLength} labels because out of range`);
|
||||
finalLabels = ['too-big'];
|
||||
|
||||
// Request changes on the PR
|
||||
await github.rest.pulls.createReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
body: `This PR is too large and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`,
|
||||
event: 'REQUEST_CHANGES'
|
||||
});
|
||||
}
|
||||
|
||||
// Add new labels
|
||||
if (finalLabels.length > 0) {
|
||||
console.log(`Adding labels: ${finalLabels.join(', ')}`);
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
labels: finalLabels
|
||||
});
|
||||
}
|
||||
|
||||
// Remove old managed labels that are no longer needed
|
||||
const labelsToRemove = managedLabels.filter(label =>
|
||||
!finalLabels.includes(label)
|
||||
);
|
||||
|
||||
for (const label of labelsToRemove) {
|
||||
console.log(`Removing label: ${label}`);
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
name: label
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`Failed to remove label ${label}:`, error.message);
|
||||
}
|
||||
}
|
||||
75
.github/workflows/ci-clang-tidy-hash.yml
vendored
75
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -1,75 +0,0 @@
|
||||
name: Clang-tidy Hash CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".clang-tidy"
|
||||
- "platformio.ini"
|
||||
- "requirements_dev.txt"
|
||||
- ".clang-tidy.hash"
|
||||
- "script/clang_tidy_hash.py"
|
||||
- ".github/workflows/ci-clang-tidy-hash.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
verify-hash:
|
||||
name: Verify clang-tidy hash
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Verify hash
|
||||
run: |
|
||||
python script/clang_tidy_hash.py --verify
|
||||
|
||||
- if: failure()
|
||||
name: Show hash details
|
||||
run: |
|
||||
python script/clang_tidy_hash.py
|
||||
echo "## Job Failed" | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
|
||||
|
||||
- if: failure()
|
||||
name: Request changes
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
await github.rest.pulls.createReview({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
event: 'REQUEST_CHANGES',
|
||||
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
|
||||
})
|
||||
|
||||
- if: success()
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
let reviews = await github.rest.pulls.listReviews({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
for (let review of reviews.data) {
|
||||
if (review.user.login === 'github-actions[bot]' && review.state === 'CHANGES_REQUESTED') {
|
||||
await github.rest.pulls.dismissReview({
|
||||
pull_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
review_id: review.id,
|
||||
message: 'Clang-tidy hash now matches configuration.'
|
||||
});
|
||||
}
|
||||
}
|
||||
4
.github/workflows/ci-docker.yml
vendored
4
.github/workflows/ci-docker.yml
vendored
@@ -47,9 +47,9 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.10"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.11.1
|
||||
uses: docker/setup-buildx-action@v3.10.0
|
||||
|
||||
- name: Set TAG
|
||||
run: |
|
||||
|
||||
284
.github/workflows/ci.yml
vendored
284
.github/workflows/ci.yml
vendored
@@ -20,8 +20,8 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.11"
|
||||
PYUPGRADE_TARGET: "--py311-plus"
|
||||
DEFAULT_PYTHON: "3.10"
|
||||
PYUPGRADE_TARGET: "--py310-plus"
|
||||
|
||||
concurrency:
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Generate cache-key
|
||||
id: cache-key
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -58,16 +58,56 @@ jobs:
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_test.txt pre-commit
|
||||
pip install -r requirements.txt -r requirements_test.txt
|
||||
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
|
||||
@@ -84,6 +124,27 @@ 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
|
||||
@@ -112,6 +173,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
@@ -127,10 +189,14 @@ 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
|
||||
@@ -138,7 +204,6 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Restore Python
|
||||
id: restore-python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -148,108 +213,56 @@ jobs:
|
||||
- name: Run pytest
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
. ./venv/Scripts/activate.ps1
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||
./venv/Scripts/activate
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests
|
||||
- name: Run pytest
|
||||
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
|
||||
determine-jobs:
|
||||
name: Determine which jobs to run
|
||||
clang-format:
|
||||
name: Check clang-format
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
outputs:
|
||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||
component-test-count: ${{ steps.determine.outputs.component-test-count }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
# Fetch enough history to find the merge base
|
||||
fetch-depth: 2
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Determine which tests to run
|
||||
id: determine
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
- name: Install clang-format
|
||||
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
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_test.txt
|
||||
pip install -e .
|
||||
- name: Register matcher
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run integration tests
|
||||
pip install clang-format -c requirements_dev.txt
|
||||
- name: Run clang-format
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
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
|
||||
- determine-jobs
|
||||
if: needs.determine-jobs.outputs.clang-tidy == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
- ruff
|
||||
- ci-custom
|
||||
- clang-format
|
||||
- flake8
|
||||
- pylint
|
||||
- pytest
|
||||
- pyupgrade
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
@@ -288,10 +301,6 @@ jobs:
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -303,14 +312,14 @@ jobs:
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
key: platformio-${{ matrix.pio_cache_key }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
key: platformio-${{ matrix.pio_cache_key }}
|
||||
|
||||
- name: Register problem matchers
|
||||
run: |
|
||||
@@ -324,28 +333,10 @@ 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
|
||||
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
|
||||
script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
|
||||
env:
|
||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
||||
@@ -355,18 +346,59 @@ 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
|
||||
- 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
|
||||
- list-components
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) > 0 && fromJSON(needs.list-components.outputs.count) < 100
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }}
|
||||
file: ${{ fromJson(needs.list-components.outputs.components) }}
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -394,8 +426,8 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
|
||||
- list-components
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100
|
||||
outputs:
|
||||
matrix: ${{ steps.split.outputs.components }}
|
||||
steps:
|
||||
@@ -404,7 +436,7 @@ jobs:
|
||||
- name: Split components into 20 groups
|
||||
id: split
|
||||
run: |
|
||||
components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
|
||||
components=$(echo '${{ needs.list-components.outputs.components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
|
||||
echo "components=$components" >> $GITHUB_OUTPUT
|
||||
|
||||
test-build-components-split:
|
||||
@@ -412,9 +444,9 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
- list-components
|
||||
- test-build-components-splitter
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
@@ -451,41 +483,23 @@ jobs:
|
||||
./script/test_build_components -e compile -c $component
|
||||
done
|
||||
|
||||
pre-commit-ci-lite:
|
||||
name: pre-commit.ci lite
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- common
|
||||
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
env:
|
||||
SKIP: pylint,clang-tidy-hash
|
||||
- uses: pre-commit-ci/lite-action@v1.1.0
|
||||
if: always()
|
||||
|
||||
ci-status:
|
||||
name: CI Status
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- ruff
|
||||
- ci-custom
|
||||
- clang-format
|
||||
- flake8
|
||||
- pylint
|
||||
- pytest
|
||||
- integration-tests
|
||||
- pyupgrade
|
||||
- clang-tidy
|
||||
- determine-jobs
|
||||
- list-components
|
||||
- test-build-components
|
||||
- test-build-components-splitter
|
||||
- test-build-components-split
|
||||
- pre-commit-ci-lite
|
||||
if: always()
|
||||
steps:
|
||||
- name: Success
|
||||
|
||||
264
.github/workflows/codeowner-review-request.yml
vendored
264
.github/workflows/codeowner-review-request.yml
vendored
@@ -1,264 +0,0 @@
|
||||
# This workflow automatically requests reviews from codeowners when:
|
||||
# 1. A PR is opened, reopened, or synchronized (updated)
|
||||
# 2. A PR is marked as ready for review
|
||||
#
|
||||
# It reads the CODEOWNERS file and matches all changed files in the PR against
|
||||
# the codeowner patterns, then requests reviews from the appropriate owners
|
||||
# while avoiding duplicate requests for users who have already been requested
|
||||
# or have already reviewed the PR.
|
||||
|
||||
name: Request Codeowner Reviews
|
||||
|
||||
on:
|
||||
# Needs to be pull_request_target to get write permissions
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
request-codeowner-reviews:
|
||||
name: Run
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Request reviews from component codeowners
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const pr_number = context.payload.pull_request.number;
|
||||
|
||||
console.log(`Processing PR #${pr_number} for codeowner review requests`);
|
||||
|
||||
try {
|
||||
// Get the list of changed files in this PR
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
|
||||
const changedFiles = files.map(file => file.filename);
|
||||
console.log(`Found ${changedFiles.length} changed files`);
|
||||
|
||||
if (changedFiles.length === 0) {
|
||||
console.log('No changed files found, skipping codeowner review requests');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch CODEOWNERS file from root
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'CODEOWNERS',
|
||||
ref: context.payload.pull_request.base.sha
|
||||
});
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
|
||||
// Parse CODEOWNERS file to extract all patterns and their owners
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
const codeownersPatterns = [];
|
||||
|
||||
// Convert CODEOWNERS pattern to regex (robust glob handling)
|
||||
function globToRegex(pattern) {
|
||||
// Escape regex special characters except for glob wildcards
|
||||
let regexStr = pattern
|
||||
.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars
|
||||
.replace(/\*\*/g, '.*') // globstar
|
||||
.replace(/\*/g, '[^/]*') // single star
|
||||
.replace(/\?/g, '.'); // question mark
|
||||
return new RegExp('^' + regexStr + '$');
|
||||
}
|
||||
|
||||
// Helper function to create comment body
|
||||
function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) {
|
||||
const reviewerMentions = reviewersList.map(r => `@${r}`);
|
||||
const teamMentions = teamsList.map(t => `@${owner}/${t}`);
|
||||
const allMentions = [...reviewerMentions, ...teamMentions].join(', ');
|
||||
|
||||
if (isSuccessful) {
|
||||
return `👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! 🙏`;
|
||||
} else {
|
||||
return `👋 Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! 🙏\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`;
|
||||
}
|
||||
}
|
||||
|
||||
for (const line of codeownersLines) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
// Use robust glob-to-regex conversion
|
||||
const regex = globToRegex(pattern);
|
||||
codeownersPatterns.push({ pattern, regex, owners });
|
||||
}
|
||||
|
||||
console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
|
||||
|
||||
// Match changed files against CODEOWNERS patterns
|
||||
const matchedOwners = new Set();
|
||||
const matchedTeams = new Set();
|
||||
const fileMatches = new Map(); // Track which files matched which patterns
|
||||
|
||||
for (const file of changedFiles) {
|
||||
for (const { pattern, regex, owners } of codeownersPatterns) {
|
||||
if (regex.test(file)) {
|
||||
console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`);
|
||||
|
||||
if (!fileMatches.has(file)) {
|
||||
fileMatches.set(file, []);
|
||||
}
|
||||
fileMatches.get(file).push({ pattern, owners });
|
||||
|
||||
// Add owners to the appropriate set (remove @ prefix)
|
||||
for (const owner of owners) {
|
||||
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
|
||||
if (cleanOwner.includes('/')) {
|
||||
// Team mention (org/team-name)
|
||||
const teamName = cleanOwner.split('/')[1];
|
||||
matchedTeams.add(teamName);
|
||||
} else {
|
||||
// Individual user
|
||||
matchedOwners.add(cleanOwner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedOwners.size === 0 && matchedTeams.size === 0) {
|
||||
console.log('No codeowners found for any changed files');
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the PR author from reviewers
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
matchedOwners.delete(prAuthor);
|
||||
|
||||
// Get current reviewers to avoid duplicate requests (but still mention them)
|
||||
const { data: prData } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
|
||||
const currentReviewers = new Set();
|
||||
const currentTeams = new Set();
|
||||
|
||||
if (prData.requested_reviewers) {
|
||||
prData.requested_reviewers.forEach(reviewer => {
|
||||
currentReviewers.add(reviewer.login);
|
||||
});
|
||||
}
|
||||
|
||||
if (prData.requested_teams) {
|
||||
prData.requested_teams.forEach(team => {
|
||||
currentTeams.add(team.slug);
|
||||
});
|
||||
}
|
||||
|
||||
// Check for completed reviews to avoid re-requesting users who have already reviewed
|
||||
const { data: reviews } = await github.rest.pulls.listReviews({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
|
||||
const reviewedUsers = new Set();
|
||||
reviews.forEach(review => {
|
||||
reviewedUsers.add(review.user.login);
|
||||
});
|
||||
|
||||
// Remove only users who have already submitted reviews (not just requested reviewers)
|
||||
reviewedUsers.forEach(reviewer => {
|
||||
matchedOwners.delete(reviewer);
|
||||
});
|
||||
|
||||
// For teams, we'll still remove already requested teams to avoid API errors
|
||||
currentTeams.forEach(team => {
|
||||
matchedTeams.delete(team);
|
||||
});
|
||||
|
||||
const reviewersList = Array.from(matchedOwners);
|
||||
const teamsList = Array.from(matchedTeams);
|
||||
|
||||
if (reviewersList.length === 0 && teamsList.length === 0) {
|
||||
console.log('No eligible reviewers found (all may already be requested or reviewed)');
|
||||
return;
|
||||
}
|
||||
|
||||
const totalReviewers = reviewersList.length + teamsList.length;
|
||||
console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`);
|
||||
|
||||
// Request reviews
|
||||
try {
|
||||
const requestParams = {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
};
|
||||
|
||||
// Filter out users who are already requested reviewers for the API call
|
||||
const newReviewers = reviewersList.filter(reviewer => !currentReviewers.has(reviewer));
|
||||
const newTeams = teamsList.filter(team => !currentTeams.has(team));
|
||||
|
||||
if (newReviewers.length > 0) {
|
||||
requestParams.reviewers = newReviewers;
|
||||
}
|
||||
|
||||
if (newTeams.length > 0) {
|
||||
requestParams.team_reviewers = newTeams;
|
||||
}
|
||||
|
||||
// Only make the API call if there are new reviewers to request
|
||||
if (newReviewers.length > 0 || newTeams.length > 0) {
|
||||
await github.rest.pulls.requestReviewers(requestParams);
|
||||
console.log(`Successfully requested reviews from ${newReviewers.length} new users and ${newTeams.length} new teams`);
|
||||
} else {
|
||||
console.log('All codeowners are already requested reviewers or have reviewed');
|
||||
}
|
||||
|
||||
// Add a comment to the PR mentioning what happened (include all matched codeowners)
|
||||
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body: commentBody
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status === 422) {
|
||||
console.log('Some reviewers may already be requested or unavailable:', error.message);
|
||||
|
||||
// Try to add a comment even if review request failed
|
||||
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
|
||||
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body: commentBody
|
||||
});
|
||||
} catch (commentError) {
|
||||
console.log('Failed to add comment:', commentError.message);
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log('Failed to process codeowner review requests:', error.message);
|
||||
console.error(error);
|
||||
}
|
||||
147
.github/workflows/external-component-bot.yml
vendored
147
.github/workflows/external-component-bot.yml
vendored
@@ -1,147 +0,0 @@
|
||||
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 = "<!-- This comment was generated automatically by a GitHub workflow. -->";
|
||||
let commentBody;
|
||||
if (esphomeChanges.length === 1) {
|
||||
commentBody = generateExternalComponentInstructions(prNumber, componentChanges, owner, repo);
|
||||
} else {
|
||||
commentBody = generateRepoInstructions(prNumber, owner, repo, context.payload.pull_request.head.ref);
|
||||
}
|
||||
commentBody += `\n\n---\n(Added by the PR bot)\n\n${commentMarker}`;
|
||||
|
||||
// Check for existing bot comment
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
|
||||
const botComment = comments.data.find(comment =>
|
||||
comment.body.includes(commentMarker)
|
||||
);
|
||||
|
||||
if (botComment && botComment.body === commentBody) {
|
||||
// No changes in the comment, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
if (botComment) {
|
||||
// Update existing comment
|
||||
await github.rest.issues.updateComment({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
comment_id: botComment.id,
|
||||
body: commentBody,
|
||||
});
|
||||
} else {
|
||||
// Create new comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
issue_number: prNumber,
|
||||
body: commentBody,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getEsphomeAndComponentChanges(github, owner, repo, prNumber) {
|
||||
const changedFiles = await github.rest.pulls.listFiles({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
const esphomeChanges = changedFiles.data
|
||||
.filter(file => file.filename !== "esphome/core/defines.h" && file.filename.startsWith('esphome/'))
|
||||
.map(file => {
|
||||
const match = file.filename.match(/esphome\/([^/]+)/);
|
||||
return match ? match[1] : null;
|
||||
})
|
||||
.filter(it => it !== null);
|
||||
|
||||
if (esphomeChanges.length === 0) {
|
||||
return {esphomeChanges: [], componentChanges: []};
|
||||
}
|
||||
|
||||
const uniqueEsphomeChanges = [...new Set(esphomeChanges)];
|
||||
const componentChanges = changedFiles.data
|
||||
.filter(file => file.filename.startsWith('esphome/components/'))
|
||||
.map(file => {
|
||||
const match = file.filename.match(/esphome\/components\/([^/]+)\//);
|
||||
return match ? match[1] : null;
|
||||
})
|
||||
.filter(it => it !== null);
|
||||
|
||||
return {esphomeChanges: uniqueEsphomeChanges, componentChanges: [...new Set(componentChanges)]};
|
||||
}
|
||||
|
||||
// Start of main code.
|
||||
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const {owner, repo} = context.repo;
|
||||
|
||||
const {esphomeChanges, componentChanges} = await getEsphomeAndComponentChanges(github, owner, repo, prNumber);
|
||||
if (componentChanges.length !== 0) {
|
||||
await createComment(github, owner, repo, prNumber, esphomeChanges, componentChanges);
|
||||
}
|
||||
119
.github/workflows/issue-codeowner-notify.yml
vendored
119
.github/workflows/issue-codeowner-notify.yml
vendored
@@ -1,119 +0,0 @@
|
||||
# This workflow automatically notifies codeowners when an issue is labeled with component labels.
|
||||
# It reads the CODEOWNERS file to find the maintainers for the labeled components
|
||||
# and posts a comment mentioning them to ensure they're aware of the issue.
|
||||
|
||||
name: Notify Issue Codeowners
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
notify-codeowners:
|
||||
name: Run
|
||||
if: ${{ startsWith(github.event.label.name, format('component{0} ', ':')) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify codeowners for component issues
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const issue_number = context.payload.issue.number;
|
||||
const labelName = context.payload.label.name;
|
||||
|
||||
console.log(`Processing issue #${issue_number} with label: ${labelName}`);
|
||||
|
||||
// Extract component name from label
|
||||
const componentName = labelName.replace('component: ', '');
|
||||
console.log(`Component: ${componentName}`);
|
||||
|
||||
try {
|
||||
// Fetch CODEOWNERS file from root
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'CODEOWNERS'
|
||||
});
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
|
||||
// Parse CODEOWNERS file to extract component mappings
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
let componentOwners = null;
|
||||
|
||||
for (const line of codeownersLines) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
// Look for component patterns: esphome/components/{component}/*
|
||||
const componentMatch = pattern.match(/^esphome\/components\/([^\/]+)\/\*$/);
|
||||
if (componentMatch && componentMatch[1] === componentName) {
|
||||
componentOwners = owners;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!componentOwners) {
|
||||
console.log(`No codeowners found for component: ${componentName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found codeowners for '${componentName}': ${componentOwners.join(', ')}`);
|
||||
|
||||
// Separate users and teams
|
||||
const userOwners = [];
|
||||
const teamOwners = [];
|
||||
|
||||
for (const owner of componentOwners) {
|
||||
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
|
||||
if (cleanOwner.includes('/')) {
|
||||
// Team mention (org/team-name)
|
||||
teamOwners.push(`@${cleanOwner}`);
|
||||
} else {
|
||||
// Individual user
|
||||
userOwners.push(`@${cleanOwner}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove issue author from mentions to avoid self-notification
|
||||
const issueAuthor = context.payload.issue.user.login;
|
||||
const filteredUserOwners = userOwners.filter(mention =>
|
||||
mention !== `@${issueAuthor}`
|
||||
);
|
||||
|
||||
const allMentions = [...filteredUserOwners, ...teamOwners];
|
||||
|
||||
if (allMentions.length === 0) {
|
||||
console.log('No codeowners to notify (issue author is the only codeowner)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create comment body
|
||||
const mentionString = allMentions.join(', ');
|
||||
const commentBody = `👋 Hey ${mentionString}!\n\nThis issue has been labeled with \`component: ${componentName}\` and you've been identified as a codeowner of this component. Please take a look when you have a chance!\n\nThanks for maintaining this component! 🙏`;
|
||||
|
||||
// Post comment
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue_number,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
console.log(`Successfully notified codeowners: ${mentionString}`);
|
||||
|
||||
} catch (error) {
|
||||
console.log('Failed to process codeowner notifications:', error.message);
|
||||
console.error(error);
|
||||
}
|
||||
23
.github/workflows/lock.yml
vendored
23
.github/workflows/lock.yml
vendored
@@ -1,11 +1,28 @@
|
||||
---
|
||||
name: Lock closed issues and PRs
|
||||
name: Lock
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 0 * * *" # Run daily at 00:30 UTC
|
||||
- cron: "30 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: lock
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@main
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5.0.1
|
||||
with:
|
||||
pr-inactive-days: "1"
|
||||
pr-lock-reason: ""
|
||||
exclude-any-pr-labels: keep-open
|
||||
|
||||
issue-inactive-days: "7"
|
||||
issue-lock-reason: ""
|
||||
exclude-any-issue-labels: keep-open
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -96,10 +96,10 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.11.1
|
||||
uses: docker/setup-buildx-action@v3.10.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@v3.4.0
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.11.1
|
||||
uses: docker/setup-buildx-action@v3.10.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
|
||||
25
.github/workflows/yaml-lint.yml
vendored
Normal file
25
.github/workflows/yaml-lint.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
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
|
||||
@@ -1,17 +1,10 @@
|
||||
---
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
|
||||
ci:
|
||||
autoupdate_commit_msg: 'pre-commit: autoupdate'
|
||||
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
|
||||
# Skip hooks that have issues in pre-commit CI environment
|
||||
skip: [pylint, clang-tidy-hash]
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.4
|
||||
rev: v0.11.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -19,7 +12,7 @@ repos:
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 7.3.0
|
||||
rev: 7.2.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
@@ -27,25 +20,22 @@ repos:
|
||||
- pydocstyle==5.1.1
|
||||
files: ^(esphome|tests)/.+\.py$
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v3.4.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: [--py311-plus]
|
||||
args: [--py310-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:
|
||||
@@ -58,10 +48,3 @@ 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: []
|
||||
|
||||
20
CODEOWNERS
20
CODEOWNERS
@@ -9,7 +9,6 @@
|
||||
pyproject.toml @esphome/core
|
||||
esphome/*.py @esphome/core
|
||||
esphome/core/* @esphome/core
|
||||
.github/** @esphome/core
|
||||
|
||||
# Integrations
|
||||
esphome/components/a01nyub/* @MrSuicideParrot
|
||||
@@ -29,7 +28,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 @precurse
|
||||
esphome/components/airthings_wave_plus/* @jeromelaban
|
||||
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
|
||||
esphome/components/alpha3/* @jan-hofmeier
|
||||
esphome/components/am2315c/* @swoboda1337
|
||||
@@ -88,7 +87,6 @@ esphome/components/bp1658cj/* @Cossid
|
||||
esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/button/* @esphome/core
|
||||
esphome/components/bytebuffer/* @clydebarrow
|
||||
esphome/components/camera/* @DT-art1 @bdraco
|
||||
esphome/components/canbus/* @danielschramm @mvturnho
|
||||
esphome/components/cap1188/* @mreditor97
|
||||
esphome/components/captive_portal/* @OttoWinter
|
||||
@@ -126,7 +124,6 @@ esphome/components/dht/* @OttoWinter
|
||||
esphome/components/display_menu_base/* @numo68
|
||||
esphome/components/dps310/* @kbx81
|
||||
esphome/components/ds1307/* @badbadc0ffee
|
||||
esphome/components/ds2484/* @mrk-its
|
||||
esphome/components/dsmr/* @glmnet @zuidwijk
|
||||
esphome/components/duty_time/* @dudanov
|
||||
esphome/components/ee895/* @Stock-M
|
||||
@@ -149,7 +146,6 @@ esphome/components/esp32_ble_client/* @jesserockz
|
||||
esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz
|
||||
esphome/components/esp32_camera_web_server/* @ayufan
|
||||
esphome/components/esp32_can/* @Sympatron
|
||||
esphome/components/esp32_hosted/* @swoboda1337
|
||||
esphome/components/esp32_improv/* @jesserockz
|
||||
esphome/components/esp32_rmt/* @jesserockz
|
||||
esphome/components/esp32_rmt_led_strip/* @jesserockz
|
||||
@@ -171,7 +167,6 @@ esphome/components/ft5x06/* @clydebarrow
|
||||
esphome/components/ft63x6/* @gpambrozio
|
||||
esphome/components/gcja5/* @gcormier
|
||||
esphome/components/gdk101/* @Szewcson
|
||||
esphome/components/gl_r01_i2c/* @pkejval
|
||||
esphome/components/globals/* @esphome/core
|
||||
esphome/components/gp2y1010au0f/* @zry98
|
||||
esphome/components/gp8403/* @jesserockz
|
||||
@@ -252,11 +247,9 @@ esphome/components/libretiny_pwm/* @kuba2k2
|
||||
esphome/components/light/* @esphome/core
|
||||
esphome/components/lightwaverf/* @max246
|
||||
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
|
||||
esphome/components/ln882x/* @lamauny
|
||||
esphome/components/lock/* @esphome/core
|
||||
esphome/components/logger/* @esphome/core
|
||||
esphome/components/logger/select/* @clydebarrow
|
||||
esphome/components/lps22/* @nagisa
|
||||
esphome/components/ltr390/* @latonita @sjtrny
|
||||
esphome/components/ltr501/* @latonita
|
||||
esphome/components/ltr_als_ps/* @latonita
|
||||
@@ -325,13 +318,11 @@ 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
|
||||
esphome/components/opentherm/* @olegtarasov
|
||||
esphome/components/openthread/* @mrene
|
||||
esphome/components/opt3001/* @ccutrer
|
||||
esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/packet_transport/* @clydebarrow
|
||||
@@ -339,7 +330,6 @@ esphome/components/pca6416a/* @Mat931
|
||||
esphome/components/pca9554/* @clydebarrow @hwstar
|
||||
esphome/components/pcf85063/* @brogon
|
||||
esphome/components/pcf8563/* @KoenBreeman
|
||||
esphome/components/pi4ioe5v6408/* @jesserockz
|
||||
esphome/components/pid/* @OttoWinter
|
||||
esphome/components/pipsolar/* @andreashergert1984
|
||||
esphome/components/pm1006/* @habbie
|
||||
@@ -380,7 +370,6 @@ 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
|
||||
@@ -447,8 +436,6 @@ esphome/components/sun/* @OttoWinter
|
||||
esphome/components/sun_gtil2/* @Mat931
|
||||
esphome/components/switch/* @esphome/core
|
||||
esphome/components/switch/binary_sensor/* @ssieb
|
||||
esphome/components/sx126x/* @swoboda1337
|
||||
esphome/components/sx127x/* @swoboda1337
|
||||
esphome/components/syslog/* @clydebarrow
|
||||
esphome/components/t6615/* @tylermenezes
|
||||
esphome/components/tc74/* @sethgirvan
|
||||
@@ -503,11 +490,10 @@ esphome/components/vbus/* @ssieb
|
||||
esphome/components/veml3235/* @kbx81
|
||||
esphome/components/veml7700/* @latonita
|
||||
esphome/components/version/* @esphome/core
|
||||
esphome/components/voice_assistant/* @jesserockz @kahrendt
|
||||
esphome/components/voice_assistant/* @jesserockz
|
||||
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_idf/* @dentra
|
||||
esphome/components/weikai/* @DrCoolZic
|
||||
@@ -534,10 +520,8 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl
|
||||
esphome/components/xiaomi_mhoc303/* @drug123
|
||||
esphome/components/xiaomi_mhoc401/* @vevsvevs
|
||||
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
|
||||
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
|
||||
|
||||
@@ -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/esphome/issues) -- [Feature requests](https://github.com/orgs/esphome/discussions)
|
||||
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues)
|
||||
|
||||
---
|
||||
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.8.0-dev
|
||||
PROJECT_NUMBER = 2025.7.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
---
|
||||
|
||||
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/esphome/issues) -- [Feature requests](https://github.com/orgs/esphome/discussions)
|
||||
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -34,9 +34,11 @@ from esphome.const import (
|
||||
CONF_PORT,
|
||||
CONF_SUBSTITUTIONS,
|
||||
CONF_TOPIC,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_RTL87XX,
|
||||
SECRETS_FILES,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError, coroutine
|
||||
@@ -352,7 +354,7 @@ def upload_program(config, args, host):
|
||||
if CORE.target_platform in (PLATFORM_RP2040):
|
||||
return upload_using_platformio(config, args.device)
|
||||
|
||||
if CORE.is_libretiny:
|
||||
if CORE.target_platform in (PLATFORM_BK72XX, PLATFORM_RTL87XX):
|
||||
return upload_using_platformio(config, host)
|
||||
|
||||
return 1 # Unknown target platform
|
||||
|
||||
@@ -22,7 +22,6 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
TemplateArguments,
|
||||
add,
|
||||
add_build_flag,
|
||||
add_build_unflag,
|
||||
add_define,
|
||||
add_global,
|
||||
add_library,
|
||||
@@ -35,7 +34,6 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
process_lambda,
|
||||
progmem_array,
|
||||
safe_exp,
|
||||
set_cpp_standard,
|
||||
statement,
|
||||
static_const_array,
|
||||
templatable,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <core_esp8266_waveform.h>
|
||||
@@ -194,17 +193,18 @@ void AcDimmer::setup() {
|
||||
setTimer1Callback(&timer_interrupt);
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
// timer frequency of 1mhz
|
||||
dimmer_timer = timerBegin(1000000);
|
||||
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
|
||||
// 80 Divider -> 1 count=1µs
|
||||
dimmer_timer = timerBegin(0, 80, true);
|
||||
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true);
|
||||
// For ESP32, we can't use dynamic interval calculation because the timerX functions
|
||||
// are not callable from ISR (placed in flash storage).
|
||||
// Here we just use an interrupt firing every 50 µs.
|
||||
timerAlarm(dimmer_timer, 50, true, 0);
|
||||
timerAlarmWrite(dimmer_timer, 50, true);
|
||||
timerAlarmEnable(dimmer_timer);
|
||||
#endif
|
||||
}
|
||||
void AcDimmer::write_state(float state) {
|
||||
state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
|
||||
state = std::acos(1 - (2 * state)) / 3.14159; // RMS power compensation
|
||||
auto new_value = static_cast<uint16_t>(roundf(state * 65535));
|
||||
if (new_value != 0 && this->store_.value == 0)
|
||||
this->store_.init_cycle = this->init_with_half_cycle_;
|
||||
|
||||
@@ -5,21 +5,13 @@ from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ANALOG,
|
||||
CONF_INPUT,
|
||||
CONF_NUMBER,
|
||||
PLATFORM_ESP8266,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -52,93 +44,82 @@ SAMPLING_MODES = {
|
||||
"max": sampling_mode.MAX,
|
||||
}
|
||||
|
||||
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)
|
||||
adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
|
||||
adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
|
||||
|
||||
# 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: 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,
|
||||
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,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32C2: {
|
||||
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,
|
||||
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,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
|
||||
VARIANT_ESP32C3: {
|
||||
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,
|
||||
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,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
|
||||
VARIANT_ESP32C6: {
|
||||
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,
|
||||
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,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32H2: {
|
||||
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,
|
||||
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,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32S2: {
|
||||
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,
|
||||
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,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
|
||||
VARIANT_ESP32S3: {
|
||||
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,
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -147,56 +128,54 @@ 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: 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,
|
||||
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,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32C2: {
|
||||
5: adc_channel_t.ADC_CHANNEL_0,
|
||||
5: adc2_channel_t.ADC2_CHANNEL_0,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
|
||||
VARIANT_ESP32C3: {
|
||||
5: adc_channel_t.ADC_CHANNEL_0,
|
||||
5: adc2_channel_t.ADC2_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: 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,
|
||||
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,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
|
||||
VARIANT_ESP32S3: {
|
||||
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,
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -250,20 +229,3 @@ def validate_adc_pin(value):
|
||||
)(value)
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
{
|
||||
"adc_sensor_esp32.cpp": {
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
},
|
||||
"adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
|
||||
"adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
|
||||
"adc_sensor_libretiny.cpp": {
|
||||
PlatformFramework.BK72XX_ARDUINO,
|
||||
PlatformFramework.RTL87XX_ARDUINO,
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,22 +3,20 @@
|
||||
#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 "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
|
||||
#include <esp_adc_cal.h>
|
||||
#include "driver/adc.h"
|
||||
#endif // USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
#ifdef USE_ESP32
|
||||
// clang-format off
|
||||
#if (ESP_IDF_VERSION_MAJOR == 5 && \
|
||||
#if (ESP_IDF_VERSION_MAJOR == 4 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 7)) || \
|
||||
(ESP_IDF_VERSION_MAJOR == 5 && \
|
||||
((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \
|
||||
(ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \
|
||||
(ESP_IDF_VERSION_MINOR >= 2)) \
|
||||
@@ -30,127 +28,79 @@ static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11;
|
||||
#endif
|
||||
#endif // USE_ESP32
|
||||
|
||||
enum class SamplingMode : uint8_t {
|
||||
AVG = 0,
|
||||
MIN = 1,
|
||||
MAX = 2,
|
||||
};
|
||||
|
||||
enum class SamplingMode : uint8_t { AVG = 0, MIN = 1, MAX = 2 };
|
||||
const LogString *sampling_mode_to_str(SamplingMode mode);
|
||||
|
||||
class Aggregator {
|
||||
public:
|
||||
Aggregator(SamplingMode mode);
|
||||
void add_sample(uint32_t value);
|
||||
uint32_t aggregate();
|
||||
Aggregator(SamplingMode mode);
|
||||
|
||||
protected:
|
||||
SamplingMode mode_{SamplingMode::AVG};
|
||||
uint32_t aggr_{0};
|
||||
uint32_t samples_{0};
|
||||
SamplingMode mode_{SamplingMode::AVG};
|
||||
};
|
||||
|
||||
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
|
||||
public:
|
||||
/// Update the sensor's state by reading the current ADC value.
|
||||
/// This method is called periodically based on the update interval.
|
||||
void update() override;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Return the setup priority for this component.
|
||||
/// Components with higher priority are initialized earlier during setup.
|
||||
/// @return A float representing the setup priority.
|
||||
float get_setup_priority() const override;
|
||||
|
||||
/// Set the GPIO pin to be used by the ADC sensor.
|
||||
/// @param pin Pointer to an InternalGPIOPin representing the ADC input pin.
|
||||
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
|
||||
|
||||
/// Enable or disable the output of raw ADC values (unprocessed data).
|
||||
/// @param output_raw Boolean indicating whether to output raw ADC values (true) or processed values (false).
|
||||
void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; }
|
||||
|
||||
/// Set the number of samples to be taken for ADC readings to improve accuracy.
|
||||
/// A higher sample count reduces noise but increases the reading time.
|
||||
/// @param sample_count The number of samples (e.g., 1, 4, 8).
|
||||
void set_sample_count(uint8_t sample_count);
|
||||
|
||||
/// Set the sampling mode for how multiple ADC samples are combined into a single measurement.
|
||||
///
|
||||
/// When multiple samples are taken (controlled by set_sample_count), they can be combined
|
||||
/// in one of three ways:
|
||||
/// - SamplingMode::AVG: Compute the average (default)
|
||||
/// - SamplingMode::MIN: Use the lowest sample value
|
||||
/// - SamplingMode::MAX: Use the highest sample value
|
||||
/// @param sampling_mode The desired sampling mode to use for aggregating ADC samples.
|
||||
void set_sampling_mode(SamplingMode sampling_mode);
|
||||
|
||||
/// Perform a single ADC sampling operation and return the measured value.
|
||||
/// This function handles raw readings, calibration, and averaging as needed.
|
||||
/// @return The sampled value as a float.
|
||||
float sample() override;
|
||||
|
||||
#ifdef USE_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).
|
||||
/// Set the attenuation for this pin. Only available on the ESP32.
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
/// Update ADC values
|
||||
void update() override;
|
||||
/// Setup ADC
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
/// `HARDWARE_LATE` setup priority
|
||||
float get_setup_priority() const override;
|
||||
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
|
||||
void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; }
|
||||
void set_sample_count(uint8_t sample_count);
|
||||
void set_sampling_mode(SamplingMode sampling_mode);
|
||||
float sample() override;
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
std::string unique_id() override;
|
||||
#endif // USE_ESP8266
|
||||
|
||||
#ifdef USE_RP2040
|
||||
void set_is_temperature() { this->is_temperature_ = true; }
|
||||
#endif // USE_RP2040
|
||||
|
||||
protected:
|
||||
uint8_t sample_count_{1};
|
||||
bool output_raw_{false};
|
||||
InternalGPIOPin *pin_;
|
||||
bool output_raw_{false};
|
||||
uint8_t sample_count_{1};
|
||||
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};
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {};
|
||||
#else
|
||||
esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {};
|
||||
#endif // ESP_IDF_VERSION_MAJOR
|
||||
#endif // USE_ESP32
|
||||
};
|
||||
|
||||
} // namespace adc
|
||||
|
||||
@@ -61,7 +61,7 @@ uint32_t Aggregator::aggregate() {
|
||||
|
||||
void ADCSensor::update() {
|
||||
float value_v = this->sample();
|
||||
ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v);
|
||||
ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v);
|
||||
this->publish_state(value_v);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,315 +8,137 @@ namespace adc {
|
||||
|
||||
static const char *const TAG = "adc.esp32";
|
||||
|
||||
adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr};
|
||||
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
#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
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
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());
|
||||
// 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;
|
||||
|
||||
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_);
|
||||
}
|
||||
}
|
||||
this->adc_handle_ = ADCSensor::shared_adc_handles[this->adc_unit_];
|
||||
|
||||
this->setup_flags_.handle_init_complete = true;
|
||||
|
||||
adc_oneshot_chan_cfg_t config = {
|
||||
.atten = this->attenuation_,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
};
|
||||
esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error configuring channel: %d", err);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->setup_flags_.config_complete = true;
|
||||
|
||||
// Initialize ADC calibration
|
||||
if (this->calibration_handle_ == nullptr) {
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
esp_err_t err;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
// RISC-V variants and S3 use curve fitting calibration
|
||||
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.chan = this->channel_;
|
||||
#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.unit_id = this->adc_unit_;
|
||||
cali_config.atten = this->attenuation_;
|
||||
cali_config.bitwidth = ADC_BITWIDTH_DEFAULT;
|
||||
|
||||
err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
|
||||
if (err == ESP_OK) {
|
||||
this->calibration_handle_ = handle;
|
||||
this->setup_flags_.calibration_complete = true;
|
||||
ESP_LOGV(TAG, "Using curve fitting calibration");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err);
|
||||
this->setup_flags_.calibration_complete = false;
|
||||
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;
|
||||
}
|
||||
#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_);
|
||||
if (this->autorange_) {
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: auto");
|
||||
} else {
|
||||
switch (this->attenuation_) {
|
||||
case ADC_ATTEN_DB_0:
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: 0db");
|
||||
break;
|
||||
case ADC_ATTEN_DB_2_5:
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: 2.5db");
|
||||
break;
|
||||
case ADC_ATTEN_DB_6:
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: 6db");
|
||||
break;
|
||||
case ADC_ATTEN_DB_12_COMPAT:
|
||||
ESP_LOGCONFIG(TAG, " Attenuation: 12db");
|
||||
break;
|
||||
default: // This is to satisfy the unused ADC_ATTEN_MAX
|
||||
break;
|
||||
}
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Channel: %d\n"
|
||||
" Unit: %s\n"
|
||||
" Attenuation: %s\n"
|
||||
" Samples: %i\n"
|
||||
" Samples: %i\n"
|
||||
" Sampling mode: %s",
|
||||
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");
|
||||
|
||||
this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
float ADCSensor::sample() {
|
||||
if (this->autorange_) {
|
||||
return this->sample_autorange_();
|
||||
} else {
|
||||
return this->sample_fixed_attenuation_();
|
||||
}
|
||||
}
|
||||
if (!this->autorange_) {
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
|
||||
float ADCSensor::sample_fixed_attenuation_() {
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
aggr.add_sample(raw);
|
||||
}
|
||||
|
||||
if (raw == -1) {
|
||||
ESP_LOGW(TAG, "Invalid ADC reading");
|
||||
continue;
|
||||
if (this->output_raw_) {
|
||||
return aggr.aggregate();
|
||||
}
|
||||
|
||||
aggr.add_sample(raw);
|
||||
uint32_t mv =
|
||||
esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]);
|
||||
return mv / 1000.0f;
|
||||
}
|
||||
|
||||
uint32_t final_value = aggr.aggregate();
|
||||
int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
|
||||
|
||||
if (this->output_raw_) {
|
||||
return final_value;
|
||||
}
|
||||
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
int voltage_mv;
|
||||
esp_err_t err = adc_cali_raw_to_voltage(this->calibration_handle_, final_value, &voltage_mv);
|
||||
if (err == ESP_OK) {
|
||||
return voltage_mv / 1000.0f;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else // Other ESP32 variants use line fitting calibration
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
|
||||
this->calibration_handle_ = nullptr;
|
||||
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_);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return final_value * 3.3f / 4095.0f;
|
||||
}
|
||||
|
||||
float ADCSensor::sample_autorange_() {
|
||||
// Auto-range mode
|
||||
auto read_atten = [this](adc_atten_t atten) -> std::pair<int, float> {
|
||||
// First reconfigure the attenuation for this reading
|
||||
adc_oneshot_chan_cfg_t config = {
|
||||
.atten = atten,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
};
|
||||
|
||||
esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Error configuring ADC channel for autorange: %d", err);
|
||||
return {-1, 0.0f};
|
||||
}
|
||||
|
||||
// Need to recalibrate for the new attenuation
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
// Delete old calibration handle
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
#endif
|
||||
this->calibration_handle_ = nullptr;
|
||||
}
|
||||
|
||||
// Create new calibration handle for this attenuation
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
adc_cali_curve_fitting_config_t cali_config = {};
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.chan = this->channel_;
|
||||
#endif
|
||||
cali_config.unit_id = this->adc_unit_;
|
||||
cali_config.atten = atten;
|
||||
cali_config.bitwidth = ADC_BITWIDTH_DEFAULT;
|
||||
|
||||
err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
|
||||
#else
|
||||
adc_cali_line_fitting_config_t cali_config = {
|
||||
.unit_id = this->adc_unit_,
|
||||
.atten = atten,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
.default_vref = 1100,
|
||||
#endif
|
||||
};
|
||||
err = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
|
||||
#endif
|
||||
|
||||
int raw;
|
||||
err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
|
||||
if (handle != nullptr) {
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
#endif
|
||||
}
|
||||
return {-1, 0.0f};
|
||||
}
|
||||
|
||||
float voltage = 0.0f;
|
||||
if (handle != nullptr) {
|
||||
int voltage_mv;
|
||||
err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv);
|
||||
if (err == ESP_OK) {
|
||||
voltage = voltage_mv / 1000.0f;
|
||||
} else {
|
||||
voltage = raw * 3.3f / 4095.0f;
|
||||
}
|
||||
// Clean up calibration handle
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
#endif
|
||||
} else {
|
||||
voltage = raw * 3.3f / 4095.0f;
|
||||
}
|
||||
|
||||
return {raw, voltage};
|
||||
};
|
||||
|
||||
auto [raw12, mv12] = read_atten(ADC_ATTEN_DB_12);
|
||||
if (raw12 == -1) {
|
||||
ESP_LOGE(TAG, "Failed to read ADC in autorange mode");
|
||||
return NAN;
|
||||
}
|
||||
|
||||
int raw6 = 4095, raw2 = 4095, raw0 = 4095;
|
||||
float mv6 = 0, mv2 = 0, mv0 = 0;
|
||||
|
||||
if (raw12 < 4095) {
|
||||
auto [raw6_val, mv6_val] = read_atten(ADC_ATTEN_DB_6);
|
||||
raw6 = raw6_val;
|
||||
mv6 = mv6_val;
|
||||
|
||||
if (raw6 < 4095 && raw6 != -1) {
|
||||
auto [raw2_val, mv2_val] = read_atten(ADC_ATTEN_DB_2_5);
|
||||
raw2 = raw2_val;
|
||||
mv2 = mv2_val;
|
||||
|
||||
if (raw2 < 4095 && raw2 != -1) {
|
||||
auto [raw0_val, mv0_val] = read_atten(ADC_ATTEN_DB_0);
|
||||
raw0 = raw0_val;
|
||||
mv0 = mv0_val;
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,19 +147,19 @@ float ADCSensor::sample_autorange_() {
|
||||
return NAN;
|
||||
}
|
||||
|
||||
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 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);
|
||||
uint32_t csum = c12 + c6 + c2 + c0;
|
||||
|
||||
if (csum == 0) {
|
||||
ESP_LOGE(TAG, "Invalid weight sum in autorange calculation");
|
||||
return NAN;
|
||||
}
|
||||
|
||||
return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum;
|
||||
uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0);
|
||||
return mv_scaled / (float) (csum * 1000U);
|
||||
}
|
||||
|
||||
} // namespace adc
|
||||
|
||||
@@ -56,6 +56,8 @@ float ADCSensor::sample() {
|
||||
return aggr.aggregate() / 1024.0f;
|
||||
}
|
||||
|
||||
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
|
||||
|
||||
} // namespace adc
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ from esphome.const import (
|
||||
CONF_NUMBER,
|
||||
CONF_PIN,
|
||||
CONF_RAW,
|
||||
CONF_WIFI,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
|
||||
from . import (
|
||||
ATTENUATION_MODES,
|
||||
@@ -22,7 +24,6 @@ from . import (
|
||||
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
|
||||
SAMPLING_MODES,
|
||||
adc_ns,
|
||||
adc_unit_t,
|
||||
validate_adc_pin,
|
||||
)
|
||||
|
||||
@@ -56,6 +57,21 @@ 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
|
||||
)
|
||||
@@ -83,6 +99,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_config,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate_config
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
@@ -101,13 +119,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 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))
|
||||
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:
|
||||
variant = get_esp32_variant()
|
||||
pin_num = config[CONF_PIN][CONF_NUMBER]
|
||||
if (
|
||||
@@ -115,10 +133,10 @@ async def to_code(config):
|
||||
and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
|
||||
):
|
||||
chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
|
||||
cg.add(var.set_channel(adc_unit_t.ADC_UNIT_1, chan))
|
||||
cg.add(var.set_channel1(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_channel(adc_unit_t.ADC_UNIT_2, chan))
|
||||
cg.add(var.set_channel2(chan))
|
||||
|
||||
@@ -85,6 +85,8 @@ class ADE7880 : public i2c::I2CDevice, public PollingComponent {
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
protected:
|
||||
ADE7880Store store_{};
|
||||
InternalGPIOPin *irq0_pin_{nullptr};
|
||||
|
||||
@@ -49,6 +49,7 @@ class ADS1115Component : public Component, public i2c::I2CDevice {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
/// HARDWARE_LATE setup priority
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; }
|
||||
|
||||
/// Helper method to request a measurement from a sensor.
|
||||
|
||||
@@ -34,6 +34,7 @@ class ADS1118 : public Component,
|
||||
ADS1118() = default;
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
/// Helper method to request a measurement from a sensor.
|
||||
float request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode);
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice {
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
/**
|
||||
* Modifies target address of AGS10.
|
||||
*
|
||||
|
||||
@@ -66,6 +66,7 @@ class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDev
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
bool set_mute_off() override;
|
||||
bool set_mute_on() override;
|
||||
|
||||
@@ -1 +1 @@
|
||||
CODEOWNERS = ["@jeromelaban", "@precurse"]
|
||||
CODEOWNERS = ["@jeromelaban"]
|
||||
|
||||
@@ -73,29 +73,11 @@ void AirthingsWavePlus::dump_config() {
|
||||
LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
|
||||
}
|
||||
|
||||
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);
|
||||
AirthingsWavePlus::AirthingsWavePlus() {
|
||||
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
|
||||
|
||||
@@ -9,20 +9,13 @@ 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:
|
||||
void setup() override;
|
||||
AirthingsWavePlus();
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
@@ -30,14 +23,12 @@ 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};
|
||||
|
||||
@@ -7,7 +7,6 @@ from esphome.const import (
|
||||
CONF_ILLUMINANCE,
|
||||
CONF_RADON,
|
||||
CONF_RADON_LONG_TERM,
|
||||
CONF_TVOC,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
ICON_RADIOACTIVE,
|
||||
@@ -16,7 +15,6 @@ from esphome.const import (
|
||||
UNIT_LUX,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = airthings_wave_base.DEPENDENCIES
|
||||
|
||||
@@ -27,59 +25,35 @@ 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,
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
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,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -99,4 +73,3 @@ 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]))
|
||||
|
||||
@@ -14,8 +14,8 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
|
||||
CODEOWNERS = ["@grahambrown11", "@hwstar"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -149,9 +149,6 @@ _ALARM_CONTROL_PANEL_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
_ALARM_CONTROL_PANEL_SCHEMA.add_extra(entity_duplicate_validator("alarm_control_panel"))
|
||||
|
||||
|
||||
def alarm_control_panel_schema(
|
||||
class_: MockObjClass,
|
||||
*,
|
||||
@@ -193,7 +190,7 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
|
||||
|
||||
|
||||
async def setup_alarm_control_panel_core_(var, config):
|
||||
await setup_entity(var, config, "alarm_control_panel")
|
||||
await setup_entity(var, config)
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
@@ -41,6 +41,7 @@ class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponen
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; }
|
||||
void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; }
|
||||
void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
|
||||
|
||||
@@ -22,6 +22,7 @@ class Am43Component : public cover::Cover, public esphome::ble_client::BLEClient
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
cover::CoverTraits get_traits() override;
|
||||
void set_pin(uint16_t pin) { this->pin_ = pin; }
|
||||
void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; }
|
||||
|
||||
@@ -22,6 +22,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void set_battery(sensor::Sensor *battery) { battery_ = battery; }
|
||||
void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; }
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
void set_sensor(sensor::Sensor *analog_sensor);
|
||||
template<typename T> void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; }
|
||||
template<typename T> void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; }
|
||||
|
||||
@@ -17,11 +17,7 @@ void Anova::setup() {
|
||||
this->current_request_ = 0;
|
||||
}
|
||||
|
||||
void Anova::loop() {
|
||||
// Parent BLEClientNode has a loop() method, but this component uses
|
||||
// polling via update() and BLE callbacks so loop isn't needed
|
||||
this->disable_loop();
|
||||
}
|
||||
void Anova::loop() {}
|
||||
|
||||
void Anova::control(const ClimateCall &call) {
|
||||
if (call.get_mode().has_value()) {
|
||||
|
||||
@@ -26,6 +26,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
climate::ClimateTraits traits() override {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.set_supports_current_temperature(true);
|
||||
|
||||
@@ -23,7 +23,7 @@ void APDS9960::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
|
||||
if (id != 0xAB && id != 0x9C && id != 0xA8) { // APDS9960 all should have one of these IDs
|
||||
this->error_code_ = WRONG_ID;
|
||||
this->mark_failed();
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,6 @@ import base64
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.config_helpers import get_logger_level
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACTION,
|
||||
@@ -24,9 +23,8 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_VARIABLES,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core import coroutine_with_priority
|
||||
|
||||
DOMAIN = "api"
|
||||
DEPENDENCIES = ["network"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
@@ -52,7 +50,6 @@ SERVICE_ARG_NATIVE_TYPES = {
|
||||
}
|
||||
CONF_ENCRYPTION = "encryption"
|
||||
CONF_BATCH_DELAY = "batch_delay"
|
||||
CONF_CUSTOM_SERVICES = "custom_services"
|
||||
|
||||
|
||||
def validate_encryption_key(value):
|
||||
@@ -113,11 +110,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
): ACTIONS_SCHEMA,
|
||||
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
|
||||
cv.Optional(CONF_ENCRYPTION): _encryption_schema,
|
||||
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
||||
),
|
||||
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_BATCH_DELAY, default="100ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
@@ -136,35 +131,27 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_port(config[CONF_PORT]))
|
||||
if config[CONF_PASSWORD]:
|
||||
cg.add_define("USE_API_PASSWORD")
|
||||
cg.add(var.set_password(config[CONF_PASSWORD]))
|
||||
cg.add(var.set_password(config[CONF_PASSWORD]))
|
||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
|
||||
|
||||
# Set USE_API_SERVICES if any services are enabled
|
||||
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
||||
cg.add_define("USE_API_SERVICES")
|
||||
|
||||
if actions := config.get(CONF_ACTIONS, []):
|
||||
for conf in actions:
|
||||
template_args = []
|
||||
func_args = []
|
||||
service_arg_names = []
|
||||
for name, var_ in conf[CONF_VARIABLES].items():
|
||||
native = SERVICE_ARG_NATIVE_TYPES[var_]
|
||||
template_args.append(native)
|
||||
func_args.append((native, name))
|
||||
service_arg_names.append(name)
|
||||
templ = cg.TemplateArguments(*template_args)
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
|
||||
)
|
||||
cg.add(var.register_user_service(trigger))
|
||||
await automation.build_automation(trigger, func_args, conf)
|
||||
for conf in config.get(CONF_ACTIONS, []):
|
||||
template_args = []
|
||||
func_args = []
|
||||
service_arg_names = []
|
||||
for name, var_ in conf[CONF_VARIABLES].items():
|
||||
native = SERVICE_ARG_NATIVE_TYPES[var_]
|
||||
template_args.append(native)
|
||||
func_args.append((native, name))
|
||||
service_arg_names.append(name)
|
||||
templ = cg.TemplateArguments(*template_args)
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
|
||||
)
|
||||
cg.add(var.register_user_service(trigger))
|
||||
await automation.build_automation(trigger, func_args, conf)
|
||||
|
||||
if CONF_ON_CLIENT_CONNECTED in config:
|
||||
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
|
||||
await automation.build_automation(
|
||||
var.get_client_connected_trigger(),
|
||||
[(cg.std_string, "client_info"), (cg.std_string, "client_address")],
|
||||
@@ -172,7 +159,6 @@ async def to_code(config):
|
||||
)
|
||||
|
||||
if CONF_ON_CLIENT_DISCONNECTED in config:
|
||||
cg.add_define("USE_API_CLIENT_DISCONNECTED_TRIGGER")
|
||||
await automation.build_automation(
|
||||
var.get_client_disconnected_trigger(),
|
||||
[(cg.std_string, "client_info"), (cg.std_string, "client_address")],
|
||||
@@ -191,7 +177,7 @@ async def to_code(config):
|
||||
# and plaintext disabled. Only a factory reset can remove it.
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
cg.add_define("USE_API_NOISE")
|
||||
cg.add_library("esphome/noise-c", "0.1.10")
|
||||
cg.add_library("esphome/noise-c", "0.1.6")
|
||||
else:
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
|
||||
@@ -320,25 +306,3 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
|
||||
@automation.register_condition("api.connected", APIConnectedCondition, {})
|
||||
async def api_connected_to_code(config, condition_id, template_arg, args):
|
||||
return cg.new_Pvariable(condition_id, template_arg)
|
||||
|
||||
|
||||
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 = []
|
||||
|
||||
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
|
||||
# This is a particularly large file that still needs to be opened and read
|
||||
# all the way to the end even when ifdef'd out
|
||||
#
|
||||
# HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set,
|
||||
# which happens when the logger level is VERY_VERBOSE
|
||||
if get_logger_level() != "VERY_VERBOSE":
|
||||
files_to_filter.append("api_pb2_dump.cpp")
|
||||
|
||||
# user_services.cpp is only needed when services are defined
|
||||
config = CORE.config.get(DOMAIN, {})
|
||||
if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]:
|
||||
files_to_filter.append("user_services.cpp")
|
||||
|
||||
return files_to_filter
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -18,13 +18,10 @@ namespace api {
|
||||
|
||||
// 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;
|
||||
|
||||
class APIConnection : public APIServerConnection {
|
||||
public:
|
||||
friend class APIServer;
|
||||
friend class ListEntitiesIterator;
|
||||
APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
|
||||
virtual ~APIConnection();
|
||||
|
||||
@@ -33,85 +30,104 @@ class APIConnection : public APIServerConnection {
|
||||
|
||||
bool send_list_info_done() {
|
||||
return this->schedule_message_(nullptr, &APIConnection::try_send_list_info_done,
|
||||
ListEntitiesDoneResponse::MESSAGE_TYPE, ListEntitiesDoneResponse::ESTIMATED_SIZE);
|
||||
ListEntitiesDoneResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor);
|
||||
void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
bool send_cover_state(cover::Cover *cover);
|
||||
void send_cover_info(cover::Cover *cover);
|
||||
void cover_command(const CoverCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
bool send_fan_state(fan::Fan *fan);
|
||||
void send_fan_info(fan::Fan *fan);
|
||||
void fan_command(const FanCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
bool send_light_state(light::LightState *light);
|
||||
void send_light_info(light::LightState *light);
|
||||
void light_command(const LightCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
bool send_sensor_state(sensor::Sensor *sensor);
|
||||
void send_sensor_info(sensor::Sensor *sensor);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
bool send_switch_state(switch_::Switch *a_switch);
|
||||
void send_switch_info(switch_::Switch *a_switch);
|
||||
void switch_command(const SwitchCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
|
||||
void send_text_sensor_info(text_sensor::TextSensor *text_sensor);
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
|
||||
void send_camera_info(esp32_camera::ESP32Camera *camera);
|
||||
void camera_image(const CameraImageRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
bool send_climate_state(climate::Climate *climate);
|
||||
void send_climate_info(climate::Climate *climate);
|
||||
void climate_command(const ClimateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
bool send_number_state(number::Number *number);
|
||||
void send_number_info(number::Number *number);
|
||||
void number_command(const NumberCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
bool send_date_state(datetime::DateEntity *date);
|
||||
void send_date_info(datetime::DateEntity *date);
|
||||
void date_command(const DateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
bool send_time_state(datetime::TimeEntity *time);
|
||||
void send_time_info(datetime::TimeEntity *time);
|
||||
void time_command(const TimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
bool send_datetime_state(datetime::DateTimeEntity *datetime);
|
||||
void send_datetime_info(datetime::DateTimeEntity *datetime);
|
||||
void datetime_command(const DateTimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool send_text_state(text::Text *text);
|
||||
void send_text_info(text::Text *text);
|
||||
void text_command(const TextCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool send_select_state(select::Select *select);
|
||||
void send_select_info(select::Select *select);
|
||||
void select_command(const SelectCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
void send_button_info(button::Button *button);
|
||||
void button_command(const ButtonCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool send_lock_state(lock::Lock *a_lock);
|
||||
void send_lock_info(lock::Lock *a_lock);
|
||||
void lock_command(const LockCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
bool send_valve_state(valve::Valve *valve);
|
||||
void send_valve_info(valve::Valve *valve);
|
||||
void valve_command(const ValveCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool send_media_player_state(media_player::MediaPlayer *media_player);
|
||||
void send_media_player_info(media_player::MediaPlayer *media_player);
|
||||
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);
|
||||
bool try_send_log_message(int level, const char *tag, const char *line);
|
||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||
if (!this->flags_.service_call_subscription)
|
||||
if (!this->service_call_subscription_)
|
||||
return;
|
||||
this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
|
||||
this->send_message(call);
|
||||
}
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
@@ -133,7 +149,7 @@ class APIConnection : public APIServerConnection {
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void send_time_request() {
|
||||
GetTimeRequest req;
|
||||
this->send_message(req, GetTimeRequest::MESSAGE_TYPE);
|
||||
this->send_message(req);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -151,22 +167,26 @@ class APIConnection : public APIServerConnection {
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
||||
void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
||||
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void send_event(event::Event *event, const std::string &event_type);
|
||||
void send_event_info(event::Event *event);
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
bool send_update_state(update::UpdateEntity *update);
|
||||
void send_update_info(update::UpdateEntity *update);
|
||||
void update_command(const UpdateCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
void on_disconnect_response(const DisconnectResponse &value) override;
|
||||
void on_ping_response(const PingResponse &value) override {
|
||||
// we initiated ping
|
||||
this->flags_.sent_ping = false;
|
||||
this->ping_retries_ = 0;
|
||||
this->sent_ping_ = false;
|
||||
}
|
||||
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
@@ -179,37 +199,31 @@ class APIConnection : public APIServerConnection {
|
||||
DeviceInfoResponse device_info(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;
|
||||
this->state_subscription_ = true;
|
||||
this->initial_state_iterator_.begin();
|
||||
}
|
||||
void subscribe_logs(const SubscribeLogsRequest &msg) override {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
this->log_subscription_ = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
|
||||
this->flags_.service_call_subscription = true;
|
||||
this->service_call_subscription_ = true;
|
||||
}
|
||||
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
GetTimeResponse get_time(const GetTimeRequest &msg) override {
|
||||
// TODO
|
||||
return {};
|
||||
}
|
||||
#ifdef USE_API_SERVICES
|
||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
|
||||
bool is_authenticated() override {
|
||||
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::AUTHENTICATED;
|
||||
}
|
||||
bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; }
|
||||
bool is_connection_setup() override {
|
||||
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED ||
|
||||
this->is_authenticated();
|
||||
return this->connection_state_ == ConnectionState ::CONNECTED || this->is_authenticated();
|
||||
}
|
||||
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
||||
void on_fatal_error() override;
|
||||
void on_unauthenticated_access() override;
|
||||
void on_no_setup_connection() override;
|
||||
@@ -226,8 +240,8 @@ class APIConnection : public APIServerConnection {
|
||||
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
|
||||
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
|
||||
shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
|
||||
// Resize to add header padding so message encoding starts at the correct position
|
||||
shared_buf.resize(header_padding);
|
||||
// Insert header padding bytes so message encoding starts at the correct position
|
||||
shared_buf.insert(shared_buf.begin(), header_padding, 0);
|
||||
return {&shared_buf};
|
||||
}
|
||||
|
||||
@@ -235,102 +249,63 @@ class APIConnection : public APIServerConnection {
|
||||
ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) {
|
||||
// Get reference to shared buffer (it maintains state between batch messages)
|
||||
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
|
||||
if (is_first_message) {
|
||||
shared_buf.clear();
|
||||
}
|
||||
|
||||
size_t current_size = shared_buf.size();
|
||||
|
||||
// Calculate padding to add:
|
||||
// - First message: just header padding
|
||||
// - Subsequent messages: footer for previous message + header padding for this message
|
||||
size_t padding_to_add = is_first_message
|
||||
? this->helper_->frame_header_padding()
|
||||
: this->helper_->frame_header_padding() + this->helper_->frame_footer_size();
|
||||
if (is_first_message) {
|
||||
// For first message, initialize buffer with header padding
|
||||
uint8_t header_padding = this->helper_->frame_header_padding();
|
||||
shared_buf.clear();
|
||||
shared_buf.reserve(message_size + header_padding);
|
||||
shared_buf.resize(header_padding);
|
||||
// Fill header padding with zeros
|
||||
std::fill(shared_buf.begin(), shared_buf.end(), 0);
|
||||
} else {
|
||||
// For subsequent messages, add footer space for previous message and header for this message
|
||||
uint8_t footer_size = this->helper_->frame_footer_size();
|
||||
uint8_t header_padding = this->helper_->frame_header_padding();
|
||||
|
||||
// Reserve space for padding + message
|
||||
shared_buf.reserve(current_size + padding_to_add + message_size);
|
||||
// Reserve additional space for everything
|
||||
shared_buf.reserve(current_size + footer_size + header_padding + message_size);
|
||||
|
||||
// Resize to add the padding bytes
|
||||
shared_buf.resize(current_size + padding_to_add);
|
||||
// Single resize to add both footer and header padding
|
||||
size_t new_size = current_size + footer_size + header_padding;
|
||||
shared_buf.resize(new_size);
|
||||
|
||||
// Fill the newly added bytes with zeros (footer + header padding)
|
||||
std::fill(shared_buf.begin() + current_size, shared_buf.end(), 0);
|
||||
}
|
||||
|
||||
return {&shared_buf};
|
||||
}
|
||||
|
||||
bool try_to_clear_buffer(bool log_out_of_space);
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint16_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_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 handle authentication completion
|
||||
void complete_authentication_();
|
||||
|
||||
// Non-template helper to encode any ProtoMessage
|
||||
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single);
|
||||
|
||||
// Helper to fill entity state base and encode message
|
||||
static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type,
|
||||
APIConnection *conn, uint32_t remaining_size, bool is_single) {
|
||||
msg.key = entity->get_object_id_hash();
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = entity->get_device_id();
|
||||
#endif
|
||||
return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single);
|
||||
}
|
||||
|
||||
// Helper to fill entity info base and encode message
|
||||
static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type,
|
||||
APIConnection *conn, uint32_t remaining_size, bool is_single) {
|
||||
// Helper function to fill common entity fields
|
||||
template<typename ResponseT> static void fill_entity_info_base(esphome::EntityBase *entity, ResponseT &response) {
|
||||
// Set common fields that are shared by all entity types
|
||||
msg.key = entity->get_object_id_hash();
|
||||
msg.object_id = entity->get_object_id();
|
||||
response.key = entity->get_object_id_hash();
|
||||
response.object_id = entity->get_object_id();
|
||||
|
||||
if (entity->has_own_name())
|
||||
msg.name = entity->get_name();
|
||||
response.name = entity->get_name();
|
||||
|
||||
// Set common EntityBase properties
|
||||
#ifdef USE_ENTITY_ICON
|
||||
msg.icon = entity->get_icon();
|
||||
#endif
|
||||
msg.disabled_by_default = entity->is_disabled_by_default();
|
||||
msg.entity_category = static_cast<enums::EntityCategory>(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);
|
||||
// Set common EntityBase properties
|
||||
response.icon = entity->get_icon();
|
||||
response.disabled_by_default = entity->is_disabled_by_default();
|
||||
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
|
||||
}
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
// Helper to check voice assistant validity and connection ownership
|
||||
inline bool check_voice_assistant_api_connection_() const;
|
||||
#endif
|
||||
|
||||
// Helper method to process multiple entities from an iterator in a batch
|
||||
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) {
|
||||
iterator.advance();
|
||||
}
|
||||
|
||||
// If the batch is full, process it immediately
|
||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
||||
if (this->deferred_batch_.size() >= MAX_INITIAL_PER_BATCH) {
|
||||
this->process_batch_();
|
||||
}
|
||||
}
|
||||
// Non-template helper to encode any ProtoMessage
|
||||
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single);
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
@@ -442,7 +417,7 @@ class APIConnection : public APIServerConnection {
|
||||
static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
#endif
|
||||
@@ -455,82 +430,127 @@ class APIConnection : public APIServerConnection {
|
||||
static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
|
||||
// Batch message method for ping requests
|
||||
static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
// Helper function to get estimated message size for buffer pre-allocation
|
||||
static uint16_t get_estimated_message_size(uint16_t message_type);
|
||||
|
||||
// === Optimal member ordering for 32-bit systems ===
|
||||
enum class ConnectionState {
|
||||
WAITING_FOR_HELLO,
|
||||
CONNECTED,
|
||||
AUTHENTICATED,
|
||||
} connection_state_{ConnectionState::WAITING_FOR_HELLO};
|
||||
|
||||
bool remove_{false};
|
||||
|
||||
// Group 1: Pointers (4 bytes each on 32-bit)
|
||||
std::unique_ptr<APIFrameHelper> helper_;
|
||||
APIServer *parent_;
|
||||
|
||||
// Group 2: Larger objects (must be 4-byte aligned)
|
||||
// These contain vectors/pointers internally, so putting them early ensures good alignment
|
||||
InitialStateIterator initial_state_iterator_;
|
||||
ListEntitiesIterator list_entities_iterator_;
|
||||
#ifdef USE_CAMERA
|
||||
std::unique_ptr<camera::CameraImageReader> image_reader_;
|
||||
#endif
|
||||
|
||||
// Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned)
|
||||
std::string client_info_;
|
||||
std::string client_peername_;
|
||||
std::string client_combined_info_;
|
||||
uint32_t client_api_version_major_{0};
|
||||
uint32_t client_api_version_minor_{0};
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
esp32_camera::CameraImageReader image_reader_;
|
||||
#endif
|
||||
|
||||
// Group 4: 4-byte types
|
||||
bool state_subscription_{false};
|
||||
int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
|
||||
uint32_t last_traffic_;
|
||||
uint32_t next_ping_retry_{0};
|
||||
uint8_t ping_retries_{0};
|
||||
bool sent_ping_{false};
|
||||
bool service_call_subscription_{false};
|
||||
bool next_close_ = false;
|
||||
APIServer *parent_;
|
||||
InitialStateIterator initial_state_iterator_;
|
||||
ListEntitiesIterator list_entities_iterator_;
|
||||
int state_subs_at_ = -1;
|
||||
|
||||
// Function pointer type for message encoding
|
||||
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single);
|
||||
|
||||
// Optimized MessageCreator class using union dispatch
|
||||
class MessageCreator {
|
||||
public:
|
||||
// Constructor for function pointer
|
||||
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
|
||||
// Constructor for function pointer (message_type = 0)
|
||||
MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; }
|
||||
|
||||
// Constructor for string state capture
|
||||
explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); }
|
||||
MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) {
|
||||
data_.string_ptr = new std::string(value);
|
||||
}
|
||||
|
||||
// No destructor - cleanup must be called explicitly with message_type
|
||||
// Destructor
|
||||
~MessageCreator() {
|
||||
// Clean up string data for string-based message types
|
||||
if (uses_string_data_()) {
|
||||
delete data_.string_ptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete copy operations - MessageCreator should only be moved
|
||||
MessageCreator(const MessageCreator &other) = delete;
|
||||
MessageCreator &operator=(const MessageCreator &other) = delete;
|
||||
// Copy constructor
|
||||
MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) {
|
||||
if (message_type_ == 0) {
|
||||
data_.ptr = other.data_.ptr;
|
||||
} else if (uses_string_data_()) {
|
||||
data_.string_ptr = new std::string(*other.data_.string_ptr);
|
||||
} else {
|
||||
data_ = other.data_; // For POD types
|
||||
}
|
||||
}
|
||||
|
||||
// Move constructor
|
||||
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; }
|
||||
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) {
|
||||
other.message_type_ = 0; // Reset other to function pointer type
|
||||
other.data_.ptr = nullptr;
|
||||
}
|
||||
|
||||
// Move assignment
|
||||
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
||||
// Assignment operators (needed for batch deduplication)
|
||||
MessageCreator &operator=(const MessageCreator &other) {
|
||||
if (this != &other) {
|
||||
// IMPORTANT: Caller must ensure cleanup() was called if this contains a string!
|
||||
// In our usage, this happens in add_item() deduplication and vector::erase()
|
||||
data_ = other.data_;
|
||||
other.data_.function_ptr = nullptr;
|
||||
// Clean up current string data if needed
|
||||
if (uses_string_data_()) {
|
||||
delete data_.string_ptr;
|
||||
}
|
||||
// Copy new data
|
||||
message_type_ = other.message_type_;
|
||||
if (other.message_type_ == 0) {
|
||||
data_.ptr = other.data_.ptr;
|
||||
} else if (other.uses_string_data_()) {
|
||||
data_.string_ptr = new std::string(*other.data_.string_ptr);
|
||||
} else {
|
||||
data_ = other.data_;
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Call operator - uses message_type to determine union type
|
||||
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
|
||||
uint8_t message_type) const;
|
||||
|
||||
// Manual cleanup method - must be called before destruction for string types
|
||||
void cleanup(uint8_t message_type) {
|
||||
#ifdef USE_EVENT
|
||||
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
|
||||
delete data_.string_ptr;
|
||||
data_.string_ptr = nullptr;
|
||||
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
||||
if (this != &other) {
|
||||
// Clean up current string data if needed
|
||||
if (uses_string_data_()) {
|
||||
delete data_.string_ptr;
|
||||
}
|
||||
// Move data
|
||||
message_type_ = other.message_type_;
|
||||
data_ = other.data_;
|
||||
// Reset other to safe state
|
||||
other.message_type_ = 0;
|
||||
other.data_.ptr = nullptr;
|
||||
}
|
||||
#endif
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Call operator
|
||||
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const;
|
||||
|
||||
private:
|
||||
union Data {
|
||||
MessageCreatorPtr function_ptr;
|
||||
std::string *string_ptr;
|
||||
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
|
||||
// Helper to check if this message type uses heap-allocated strings
|
||||
bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; }
|
||||
union CreatorData {
|
||||
MessageCreatorPtr ptr; // 8 bytes
|
||||
std::string *string_ptr; // 8 bytes
|
||||
} data_; // 8 bytes
|
||||
uint16_t message_type_; // 2 bytes (0 = function ptr, >0 = state capture)
|
||||
};
|
||||
|
||||
// Generic batching mechanism for both state updates and entity info
|
||||
@@ -538,96 +558,33 @@ class APIConnection : public APIServerConnection {
|
||||
struct BatchItem {
|
||||
EntityBase *entity; // Entity pointer
|
||||
MessageCreator creator; // Function that creates the message when needed
|
||||
uint8_t message_type; // Message type for overhead calculation (max 255)
|
||||
uint8_t estimated_size; // Estimated message size (max 255 bytes)
|
||||
uint16_t message_type; // Message type for overhead calculation
|
||||
|
||||
// Constructor for creating BatchItem
|
||||
BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size)
|
||||
: entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {}
|
||||
BatchItem(EntityBase *entity, MessageCreator creator, uint16_t message_type)
|
||||
: entity(entity), creator(std::move(creator)), message_type(message_type) {}
|
||||
};
|
||||
|
||||
std::vector<BatchItem> items;
|
||||
uint32_t batch_start_time{0};
|
||||
bool batch_scheduled{false};
|
||||
|
||||
private:
|
||||
// Helper to cleanup items from the beginning
|
||||
void cleanup_items_(size_t count) {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
items[i].creator.cleanup(items[i].message_type);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
DeferredBatch() {
|
||||
// Pre-allocate capacity for typical batch sizes to avoid reallocation
|
||||
items.reserve(8);
|
||||
}
|
||||
|
||||
~DeferredBatch() {
|
||||
// Ensure cleanup of any remaining items
|
||||
clear();
|
||||
}
|
||||
|
||||
// Add item to the batch
|
||||
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||
// Add item to the front of the batch (for high priority messages like ping)
|
||||
void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||
|
||||
// Clear all items with proper cleanup
|
||||
void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type);
|
||||
void clear() {
|
||||
cleanup_items_(items.size());
|
||||
items.clear();
|
||||
batch_scheduled = false;
|
||||
batch_start_time = 0;
|
||||
}
|
||||
|
||||
// Remove processed items from the front with proper cleanup
|
||||
void remove_front(size_t count) {
|
||||
cleanup_items_(count);
|
||||
items.erase(items.begin(), items.begin() + count);
|
||||
}
|
||||
|
||||
bool empty() const { return items.empty(); }
|
||||
size_t size() const { return items.size(); }
|
||||
const BatchItem &operator[](size_t index) const { return items[index]; }
|
||||
};
|
||||
|
||||
// DeferredBatch here (16 bytes, 4-byte aligned)
|
||||
DeferredBatch deferred_batch_;
|
||||
|
||||
// ConnectionState enum for type safety
|
||||
enum class ConnectionState : uint8_t {
|
||||
WAITING_FOR_HELLO = 0,
|
||||
CONNECTED = 1,
|
||||
AUTHENTICATED = 2,
|
||||
};
|
||||
|
||||
// Group 5: Pack all small members together to minimize padding
|
||||
// This group starts at a 4-byte boundary after DeferredBatch
|
||||
struct APIFlags {
|
||||
// Connection state only needs 2 bits (3 states)
|
||||
uint8_t connection_state : 2;
|
||||
// Log subscription needs 3 bits (log levels 0-7)
|
||||
uint8_t log_subscription : 3;
|
||||
// Boolean flags (1 bit each)
|
||||
uint8_t remove : 1;
|
||||
uint8_t state_subscription : 1;
|
||||
uint8_t sent_ping : 1;
|
||||
|
||||
uint8_t service_call_subscription : 1;
|
||||
uint8_t next_close : 1;
|
||||
uint8_t batch_scheduled : 1;
|
||||
uint8_t batch_first_message : 1; // For batch buffer allocation
|
||||
uint8_t should_try_send_immediately : 1; // True after initial states are sent
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
uint8_t log_only_mode : 1;
|
||||
#endif
|
||||
} flags_{}; // 2 bytes total
|
||||
|
||||
// 2-byte types immediately after flags_ (no padding between them)
|
||||
uint16_t client_api_version_major_{0};
|
||||
uint16_t client_api_version_minor_{0};
|
||||
// Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary
|
||||
|
||||
uint32_t get_batch_delay_ms_() const;
|
||||
// Message will use 8 more bytes than the minimum size, and typical
|
||||
// MTU is 1500. Sometimes users will see as low as 1460 MTU.
|
||||
@@ -640,72 +597,23 @@ class APIConnection : public APIServerConnection {
|
||||
// to send in one go. This is the maximum size of a single packet
|
||||
// that can be sent over the network.
|
||||
// This is to avoid fragmentation of the packet.
|
||||
static constexpr size_t MAX_BATCH_PACKET_SIZE = 1390; // MTU
|
||||
static constexpr size_t MAX_PACKET_SIZE = 1390; // MTU
|
||||
|
||||
bool schedule_batch_();
|
||||
void process_batch_();
|
||||
void clear_batch_() {
|
||||
this->deferred_batch_.clear();
|
||||
this->flags_.batch_scheduled = false;
|
||||
}
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// Helper to log a proto message from a MessageCreator object
|
||||
void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint8_t message_type) {
|
||||
this->flags_.log_only_mode = true;
|
||||
creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type);
|
||||
this->flags_.log_only_mode = false;
|
||||
}
|
||||
|
||||
void log_batch_item_(const DeferredBatch::BatchItem &item) {
|
||||
// Use the helper to log the message
|
||||
this->log_proto_message_(item.entity, item.creator, item.message_type);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Helper method to send a message either immediately or via batching
|
||||
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
|
||||
uint8_t estimated_size) {
|
||||
// Try to send immediately if:
|
||||
// 1. We should try to send immediately (should_try_send_immediately = true)
|
||||
// 2. Batch delay is 0 (user has opted in to immediate sending)
|
||||
// 3. Buffer has space available
|
||||
if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
|
||||
this->helper_->can_write_without_blocking()) {
|
||||
// Now actually encode and send
|
||||
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
|
||||
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// Log the message in verbose mode
|
||||
this->log_proto_message_(entity, MessageCreator(creator), message_type);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
// If immediate send failed, fall through to batching
|
||||
}
|
||||
|
||||
// Fall back to scheduled batching
|
||||
return this->schedule_message_(entity, creator, message_type, estimated_size);
|
||||
}
|
||||
// State for batch buffer allocation
|
||||
bool batch_first_message_{false};
|
||||
|
||||
// Helper function to schedule a deferred message with known message type
|
||||
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
|
||||
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
|
||||
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) {
|
||||
this->deferred_batch_.add_item(entity, std::move(creator), message_type);
|
||||
return this->schedule_batch_();
|
||||
}
|
||||
|
||||
// Overload for function pointers (for info messages and current state reads)
|
||||
bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type,
|
||||
uint8_t estimated_size) {
|
||||
return schedule_message_(entity, MessageCreator(function_ptr), message_type, estimated_size);
|
||||
}
|
||||
|
||||
// Helper function to schedule a high priority message at the front of the batch
|
||||
bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type,
|
||||
uint8_t estimated_size) {
|
||||
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type, estimated_size);
|
||||
return this->schedule_batch_();
|
||||
bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) {
|
||||
return schedule_message_(entity, MessageCreator(function_ptr), message_type);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "proto.h"
|
||||
#include "api_pb2_size.h"
|
||||
#include <cstring>
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -65,17 +66,6 @@ const char *api_error_to_str(APIError err) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
// Default implementation for loop - handles sending buffered data
|
||||
APIError APIFrameHelper::loop() {
|
||||
if (!this->tx_buf_.empty()) {
|
||||
APIError err = try_send_tx_buf_();
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
|
||||
}
|
||||
|
||||
// Helper method to buffer data from IOVs
|
||||
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
|
||||
SendBuffer buffer;
|
||||
@@ -224,22 +214,6 @@ APIError APIFrameHelper::init_common_() {
|
||||
}
|
||||
|
||||
#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) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Socket read failed with errno %d", errno);
|
||||
return APIError::SOCKET_READ_FAILED;
|
||||
} else if (received == 0) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Connection closed");
|
||||
return APIError::CONNECTION_CLOSED;
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
// uncomment to log raw packets
|
||||
//#define HELPER_LOG_PACKETS
|
||||
|
||||
@@ -300,21 +274,17 @@ APIError APINoiseFrameHelper::init() {
|
||||
}
|
||||
/// 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_();
|
||||
APIError err = state_action_();
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
return err;
|
||||
}
|
||||
if (!this->tx_buf_.empty()) {
|
||||
err = try_send_tx_buf_();
|
||||
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();
|
||||
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
|
||||
}
|
||||
|
||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
|
||||
@@ -342,9 +312,17 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
|
||||
// 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;
|
||||
if (received == -1) {
|
||||
if (errno == EWOULDBLOCK || errno == EAGAIN) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Socket read failed with errno %d", errno);
|
||||
return APIError::SOCKET_READ_FAILED;
|
||||
} else if (received == 0) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Connection closed");
|
||||
return APIError::CONNECTION_CLOSED;
|
||||
}
|
||||
rx_header_buf_len_ += static_cast<uint8_t>(received);
|
||||
if (static_cast<uint8_t>(received) != to_read) {
|
||||
@@ -352,15 +330,17 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
|
||||
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
|
||||
uint8_t indicator = rx_header_buf_[0];
|
||||
if (indicator != 0x01) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad indicator byte %u", indicator);
|
||||
return APIError::BAD_INDICATOR;
|
||||
}
|
||||
|
||||
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
|
||||
|
||||
if (state_ != State::DATA && msg_size > 128) {
|
||||
@@ -379,9 +359,17 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
|
||||
// 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;
|
||||
if (received == -1) {
|
||||
if (errno == EWOULDBLOCK || errno == EAGAIN) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Socket read failed with errno %d", errno);
|
||||
return APIError::SOCKET_READ_FAILED;
|
||||
} else if (received == 0) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Connection closed");
|
||||
return APIError::CONNECTION_CLOSED;
|
||||
}
|
||||
rx_buf_len_ += static_cast<uint16_t>(received);
|
||||
if (static_cast<uint16_t>(received) != to_read) {
|
||||
@@ -598,6 +586,10 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
return APIError::BAD_DATA_PACKET;
|
||||
}
|
||||
|
||||
// uint16_t type;
|
||||
// uint16_t data_len;
|
||||
// uint8_t *data;
|
||||
// uint8_t *padding; zero or more bytes to fill up the rest of the 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) {
|
||||
@@ -612,15 +604,21 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
buffer->type = type;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
|
||||
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
|
||||
uint16_t payload_len = static_cast<uint16_t>(raw_buffer->size() - frame_header_padding_);
|
||||
|
||||
// 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<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
|
||||
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
|
||||
raw_buffer->resize(raw_buffer->size() + frame_footer_size_);
|
||||
|
||||
// Use write_protobuf_packets with a single packet
|
||||
std::vector<PacketInfo> packets;
|
||||
packets.emplace_back(type, 0, payload_len);
|
||||
|
||||
return write_protobuf_packets(buffer, packets);
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
|
||||
APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector<PacketInfo> &packets) {
|
||||
APIError aerr = state_action_();
|
||||
if (aerr != APIError::OK) {
|
||||
return aerr;
|
||||
@@ -635,15 +633,18 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
||||
}
|
||||
|
||||
std::vector<uint8_t> *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) {
|
||||
uint16_t type = packet.message_type;
|
||||
uint16_t offset = packet.offset;
|
||||
uint16_t payload_len = packet.payload_size;
|
||||
uint16_t msg_len = 4 + payload_len; // type(2) + data_len(2) + payload
|
||||
|
||||
// The buffer already has padding at offset
|
||||
uint8_t *buf_start = buffer_data + packet.offset;
|
||||
uint8_t *buf_start = raw_buffer->data() + offset;
|
||||
|
||||
// Write noise header
|
||||
buf_start[0] = 0x01; // indicator
|
||||
@@ -651,10 +652,10 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
||||
|
||||
// Write message header (to be encrypted)
|
||||
const uint8_t msg_offset = 3;
|
||||
buf_start[msg_offset] = static_cast<uint8_t>(packet.message_type >> 8); // type high byte
|
||||
buf_start[msg_offset + 1] = static_cast<uint8_t>(packet.message_type); // type low byte
|
||||
buf_start[msg_offset + 2] = static_cast<uint8_t>(packet.payload_size >> 8); // data_len high byte
|
||||
buf_start[msg_offset + 3] = static_cast<uint8_t>(packet.payload_size); // data_len low byte
|
||||
buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte
|
||||
buf_start[msg_offset + 1] = (uint8_t) type; // type low byte
|
||||
buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte
|
||||
buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte
|
||||
// payload data is already in the buffer starting at offset + 7
|
||||
|
||||
// Make sure we have space for MAC
|
||||
@@ -663,8 +664,7 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
||||
// 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_);
|
||||
noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_);
|
||||
|
||||
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
|
||||
if (err != 0) {
|
||||
@@ -674,12 +674,14 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
||||
}
|
||||
|
||||
// Fill in the encrypted size
|
||||
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
|
||||
buf_start[2] = static_cast<uint8_t>(mbuf.size);
|
||||
buf_start[1] = (uint8_t) (mbuf.size >> 8);
|
||||
buf_start[2] = (uint8_t) mbuf.size;
|
||||
|
||||
// Add iovec for this encrypted packet
|
||||
this->reusable_iovs_.push_back(
|
||||
{buf_start, static_cast<size_t>(3 + mbuf.size)}); // indicator + size + encrypted data
|
||||
struct iovec iov;
|
||||
iov.iov_base = buf_start;
|
||||
iov.iov_len = 3 + mbuf.size; // indicator + size + encrypted data
|
||||
this->reusable_iovs_.push_back(iov);
|
||||
}
|
||||
|
||||
// Send all encrypted packets in one writev call
|
||||
@@ -820,12 +822,18 @@ APIError APIPlaintextFrameHelper::init() {
|
||||
state_ = State::DATA;
|
||||
return APIError::OK;
|
||||
}
|
||||
/// Not used for plaintext
|
||||
APIError APIPlaintextFrameHelper::loop() {
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
// Use base class implementation for buffer sending
|
||||
return APIFrameHelper::loop();
|
||||
if (!this->tx_buf_.empty()) {
|
||||
APIError err = try_send_tx_buf_();
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
|
||||
}
|
||||
|
||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
|
||||
@@ -854,9 +862,17 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
|
||||
// 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 (received == -1) {
|
||||
if (errno == EWOULDBLOCK || errno == EAGAIN) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Socket read failed with errno %d", errno);
|
||||
return APIError::SOCKET_READ_FAILED;
|
||||
} else if (received == 0) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Connection closed");
|
||||
return APIError::CONNECTION_CLOSED;
|
||||
}
|
||||
|
||||
// If this was the first read, validate the indicator byte
|
||||
@@ -940,9 +956,17 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
|
||||
// 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;
|
||||
if (received == -1) {
|
||||
if (errno == EWOULDBLOCK || errno == EAGAIN) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Socket read failed with errno %d", errno);
|
||||
return APIError::SOCKET_READ_FAILED;
|
||||
} else if (received == 0) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Connection closed");
|
||||
return APIError::CONNECTION_CLOSED;
|
||||
}
|
||||
rx_buf_len_ += static_cast<uint16_t>(received);
|
||||
if (static_cast<uint16_t>(received) != to_read) {
|
||||
@@ -1001,12 +1025,19 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
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<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
|
||||
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
|
||||
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
|
||||
uint16_t payload_len = static_cast<uint16_t>(raw_buffer->size() - frame_header_padding_);
|
||||
|
||||
// Use write_protobuf_packets with a single packet
|
||||
std::vector<PacketInfo> packets;
|
||||
packets.emplace_back(type, 0, payload_len);
|
||||
|
||||
return write_protobuf_packets(buffer, packets);
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer,
|
||||
const std::vector<PacketInfo> &packets) {
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
@@ -1016,15 +1047,17 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
|
||||
}
|
||||
|
||||
std::vector<uint8_t> *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) {
|
||||
uint16_t type = packet.message_type;
|
||||
uint16_t offset = packet.offset;
|
||||
uint16_t payload_len = packet.payload_size;
|
||||
|
||||
// Calculate varint sizes for header layout
|
||||
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.payload_size));
|
||||
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.message_type));
|
||||
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
|
||||
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
|
||||
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||
|
||||
// Calculate where to start writing the header
|
||||
@@ -1052,20 +1085,23 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
|
||||
//
|
||||
// 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;
|
||||
uint8_t *buf_start = raw_buffer->data() + 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);
|
||||
// Encode size varint directly into buffer
|
||||
ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
|
||||
|
||||
// Encode type varint directly into buffer
|
||||
ProtoVarInt(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<size_t>(total_header_len + packet.payload_size)});
|
||||
struct iovec iov;
|
||||
iov.iov_base = buf_start + header_offset;
|
||||
iov.iov_len = total_header_len + payload_len;
|
||||
this->reusable_iovs_.push_back(iov);
|
||||
}
|
||||
|
||||
// Send all packets in one writev call
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -30,14 +29,16 @@ struct ReadPacketBuffer {
|
||||
|
||||
// Packed packet info structure to minimize memory usage
|
||||
struct PacketInfo {
|
||||
uint16_t offset; // Offset in buffer where message starts
|
||||
uint16_t payload_size; // Size of the message payload
|
||||
uint8_t message_type; // Message type (0-255)
|
||||
uint16_t message_type; // 2 bytes
|
||||
uint16_t offset; // 2 bytes (sufficient for packet size ~1460 bytes)
|
||||
uint16_t payload_size; // 2 bytes (up to 65535 bytes)
|
||||
uint16_t padding; // 2 byte (for alignment)
|
||||
|
||||
PacketInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
|
||||
PacketInfo(uint16_t type, uint16_t off, uint16_t size)
|
||||
: message_type(type), offset(off), payload_size(size), padding(0) {}
|
||||
};
|
||||
|
||||
enum class APIError : uint16_t {
|
||||
enum class APIError : int {
|
||||
OK = 0,
|
||||
WOULD_BLOCK = 1001,
|
||||
BAD_HANDSHAKE_PACKET_LEN = 1002,
|
||||
@@ -73,7 +74,7 @@ class APIFrameHelper {
|
||||
}
|
||||
virtual ~APIFrameHelper() = default;
|
||||
virtual APIError init() = 0;
|
||||
virtual APIError loop();
|
||||
virtual APIError loop() = 0;
|
||||
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
|
||||
bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
|
||||
std::string getpeername() { return socket_->getpeername(); }
|
||||
@@ -96,11 +97,11 @@ class APIFrameHelper {
|
||||
}
|
||||
// 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;
|
||||
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
|
||||
// Write multiple protobuf packets in a single operation
|
||||
// packets contains (message_type, offset, length) for each message in the buffer
|
||||
// The buffer contains all messages with appropriate padding before each
|
||||
virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) = 0;
|
||||
virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector<PacketInfo> &packets) = 0;
|
||||
// Get the frame header padding required by this protocol
|
||||
virtual uint8_t frame_header_padding() = 0;
|
||||
// Get the frame footer size required by this protocol
|
||||
@@ -124,6 +125,38 @@ class APIFrameHelper {
|
||||
const uint8_t *current_data() const { return data.data() + offset; }
|
||||
};
|
||||
|
||||
// Queue of data buffers to be sent
|
||||
std::deque<SendBuffer> tx_buf_;
|
||||
|
||||
// Common state enum for all frame helpers
|
||||
// Note: Not all states are used by all implementations
|
||||
// - INITIALIZE: Used by both Noise and Plaintext
|
||||
// - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
|
||||
// - DATA: Used by both Noise and Plaintext
|
||||
// - CLOSED: Used by both Noise and Plaintext
|
||||
// - FAILED: Used by both Noise and Plaintext
|
||||
// - EXPLICIT_REJECT: Only used by Noise protocol
|
||||
enum class State {
|
||||
INITIALIZE = 1,
|
||||
CLIENT_HELLO = 2, // Noise only
|
||||
SERVER_HELLO = 3, // Noise only
|
||||
HANDSHAKE = 4, // Noise only
|
||||
DATA = 5,
|
||||
CLOSED = 6,
|
||||
FAILED = 7,
|
||||
EXPLICIT_REJECT = 8, // Noise only
|
||||
};
|
||||
|
||||
// Current state of the frame helper
|
||||
State state_{State::INITIALIZE};
|
||||
|
||||
// Helper name for logging
|
||||
std::string info_;
|
||||
|
||||
// Socket for communication
|
||||
socket::Socket *socket_{nullptr};
|
||||
std::unique_ptr<socket::Socket> socket_owned_;
|
||||
|
||||
// Common implementation for writing raw data to socket
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt);
|
||||
|
||||
@@ -136,47 +169,18 @@ class APIFrameHelper {
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
|
||||
const std::string &info, StateEnum &state, StateEnum failed_state);
|
||||
|
||||
// Pointers first (4 bytes each)
|
||||
socket::Socket *socket_{nullptr};
|
||||
std::unique_ptr<socket::Socket> socket_owned_;
|
||||
|
||||
// Common state enum for all frame helpers
|
||||
// Note: Not all states are used by all implementations
|
||||
// - INITIALIZE: Used by both Noise and Plaintext
|
||||
// - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
|
||||
// - DATA: Used by both Noise and Plaintext
|
||||
// - CLOSED: Used by both Noise and Plaintext
|
||||
// - FAILED: Used by both Noise and Plaintext
|
||||
// - EXPLICIT_REJECT: Only used by Noise protocol
|
||||
enum class State : uint8_t {
|
||||
INITIALIZE = 1,
|
||||
CLIENT_HELLO = 2, // Noise only
|
||||
SERVER_HELLO = 3, // Noise only
|
||||
HANDSHAKE = 4, // Noise only
|
||||
DATA = 5,
|
||||
CLOSED = 6,
|
||||
FAILED = 7,
|
||||
EXPLICIT_REJECT = 8, // Noise only
|
||||
};
|
||||
|
||||
// Containers (size varies, but typically 12+ bytes on 32-bit)
|
||||
std::deque<SendBuffer> tx_buf_;
|
||||
std::string info_;
|
||||
std::vector<struct iovec> reusable_iovs_;
|
||||
std::vector<uint8_t> rx_buf_;
|
||||
|
||||
// Group smaller types together
|
||||
uint16_t rx_buf_len_ = 0;
|
||||
State state_{State::INITIALIZE};
|
||||
uint8_t frame_header_padding_{0};
|
||||
uint8_t frame_footer_size_{0};
|
||||
// 5 bytes total, 3 bytes padding
|
||||
|
||||
// Reusable IOV array for write_protobuf_packets to avoid repeated allocations
|
||||
std::vector<struct iovec> reusable_iovs_;
|
||||
|
||||
// Receive buffer for reading frame data
|
||||
std::vector<uint8_t> rx_buf_;
|
||||
uint16_t rx_buf_len_ = 0;
|
||||
|
||||
// Common initialization for both plaintext and noise protocols
|
||||
APIError init_common_();
|
||||
|
||||
// Helper method to handle socket read results
|
||||
APIError handle_socket_read_result_(ssize_t received);
|
||||
};
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -195,8 +199,8 @@ class APINoiseFrameHelper : public APIFrameHelper {
|
||||
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<const PacketInfo> packets) override;
|
||||
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector<PacketInfo> &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
|
||||
@@ -209,28 +213,19 @@ class APINoiseFrameHelper : public APIFrameHelper {
|
||||
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<APINoiseContext> ctx_;
|
||||
|
||||
// Vector (12 bytes on 32-bit)
|
||||
std::vector<uint8_t> 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
|
||||
|
||||
std::vector<uint8_t> prologue_;
|
||||
|
||||
std::shared_ptr<APINoiseContext> ctx_;
|
||||
NoiseHandshakeState *handshake_{nullptr};
|
||||
NoiseCipherState *send_cipher_{nullptr};
|
||||
NoiseCipherState *recv_cipher_{nullptr};
|
||||
NoiseProtocolId nid_;
|
||||
};
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
@@ -249,20 +244,14 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
|
||||
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<const PacketInfo> packets) override;
|
||||
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector<PacketInfo> &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:
|
||||
@@ -274,7 +263,8 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
|
||||
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
|
||||
uint16_t rx_header_parsed_type_ = 0;
|
||||
uint16_t rx_header_parsed_len_ = 0;
|
||||
};
|
||||
#endif
|
||||
|
||||
|
||||
@@ -21,10 +21,4 @@ extend google.protobuf.MessageOptions {
|
||||
optional string ifdef = 1038;
|
||||
optional bool log = 1039 [default=true];
|
||||
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;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,8 @@
|
||||
// See script/api_protobuf/api_protobuf.py
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#include "api_pb2.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
@@ -18,11 +17,11 @@ class APIServerConnectionBase : public ProtoService {
|
||||
public:
|
||||
#endif
|
||||
|
||||
bool send_message(const ProtoMessage &msg, uint8_t message_type) {
|
||||
template<typename T> bool send_message(const T &msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_send_message_(msg.message_name(), msg.dump());
|
||||
this->log_send_message_(T::message_name(), msg.dump());
|
||||
#endif
|
||||
return this->send_message_(msg, message_type);
|
||||
return this->send_message_(msg, T::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
virtual void on_hello_request(const HelloRequest &value){};
|
||||
@@ -69,11 +68,9 @@ class APIServerConnectionBase : public ProtoService {
|
||||
virtual void on_get_time_request(const GetTimeRequest &value){};
|
||||
virtual void on_get_time_response(const GetTimeResponse &value){};
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
virtual void on_camera_image_request(const CameraImageRequest &value){};
|
||||
#endif
|
||||
|
||||
@@ -202,7 +199,7 @@ class APIServerConnectionBase : public ProtoService {
|
||||
virtual void on_update_command_request(const UpdateCommandRequest &value){};
|
||||
#endif
|
||||
protected:
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
|
||||
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
class APIServerConnection : public APIServerConnectionBase {
|
||||
@@ -218,16 +215,14 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
|
||||
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
||||
virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
|
||||
#ifdef USE_API_SERVICES
|
||||
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
virtual void button_command(const ButtonCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
virtual void camera_image(const CameraImageRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
@@ -337,16 +332,14 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
|
||||
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
void on_get_time_request(const GetTimeRequest &msg) override;
|
||||
#ifdef USE_API_SERVICES
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
void on_button_command_request(const ButtonCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
void on_camera_image_request(const CameraImageRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
|
||||
361
esphome/components/api/api_pb2_size.h
Normal file
361
esphome/components/api/api_pb2_size.h
Normal file
@@ -0,0 +1,361 @@
|
||||
#pragma once
|
||||
|
||||
#include "proto.h"
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
|
||||
class ProtoSize {
|
||||
public:
|
||||
/**
|
||||
* @brief ProtoSize class for Protocol Buffer serialization size calculation
|
||||
*
|
||||
* This class provides static methods to calculate the exact byte counts needed
|
||||
* for encoding various Protocol Buffer field types. All methods are designed to be
|
||||
* efficient for the common case where many fields have default values.
|
||||
*
|
||||
* Implements Protocol Buffer encoding size calculation according to:
|
||||
* https://protobuf.dev/programming-guides/encoding/
|
||||
*
|
||||
* Key features:
|
||||
* - Early-return optimization for zero/default values
|
||||
* - Direct total_size updates to avoid unnecessary additions
|
||||
* - Specialized handling for different field types according to protobuf spec
|
||||
* - Templated helpers for repeated fields and messages
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
||||
*
|
||||
* @param value The uint32_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(uint32_t value) {
|
||||
// Optimized varint size calculation using leading zeros
|
||||
// Each 7 bits requires one byte in the varint encoding
|
||||
if (value < 128)
|
||||
return 1; // 7 bits, common case for small values
|
||||
|
||||
// For larger values, count bytes needed based on the position of the highest bit set
|
||||
if (value < 16384) {
|
||||
return 2; // 14 bits
|
||||
} else if (value < 2097152) {
|
||||
return 3; // 21 bits
|
||||
} else if (value < 268435456) {
|
||||
return 4; // 28 bits
|
||||
} else {
|
||||
return 5; // 32 bits (maximum for uint32_t)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint64_t value as a varint
|
||||
*
|
||||
* @param value The uint64_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(uint64_t value) {
|
||||
// Handle common case of values fitting in uint32_t (vast majority of use cases)
|
||||
if (value <= UINT32_MAX) {
|
||||
return varint(static_cast<uint32_t>(value));
|
||||
}
|
||||
|
||||
// For larger values, determine size based on highest bit position
|
||||
if (value < (1ULL << 35)) {
|
||||
return 5; // 35 bits
|
||||
} else if (value < (1ULL << 42)) {
|
||||
return 6; // 42 bits
|
||||
} else if (value < (1ULL << 49)) {
|
||||
return 7; // 49 bits
|
||||
} else if (value < (1ULL << 56)) {
|
||||
return 8; // 56 bits
|
||||
} else if (value < (1ULL << 63)) {
|
||||
return 9; // 63 bits
|
||||
} else {
|
||||
return 10; // 64 bits (maximum for uint64_t)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode an int32_t value as a varint
|
||||
*
|
||||
* Special handling is needed for negative values, which are sign-extended to 64 bits
|
||||
* in Protocol Buffers, resulting in a 10-byte varint.
|
||||
*
|
||||
* @param value The int32_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(int32_t value) {
|
||||
// Negative values are sign-extended to 64 bits in protocol buffers,
|
||||
// which always results in a 10-byte varint for negative int32
|
||||
if (value < 0) {
|
||||
return 10; // Negative int32 is always 10 bytes long
|
||||
}
|
||||
// For non-negative values, use the uint32_t implementation
|
||||
return varint(static_cast<uint32_t>(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode an int64_t value as a varint
|
||||
*
|
||||
* @param value The int64_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(int64_t value) {
|
||||
// For int64_t, we convert to uint64_t and calculate the size
|
||||
// This works because the bit pattern determines the encoding size,
|
||||
// and we've handled negative int32 values as a special case above
|
||||
return varint(static_cast<uint64_t>(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a field ID and wire type
|
||||
*
|
||||
* @param field_id The field identifier
|
||||
* @param type The wire type value (from the WireType enum in the protobuf spec)
|
||||
* @return The number of bytes needed to encode the field ID and wire type
|
||||
*/
|
||||
static inline uint32_t field(uint32_t field_id, uint32_t type) {
|
||||
uint32_t tag = (field_id << 3) | (type & 0b111);
|
||||
return varint(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Common parameters for all add_*_field methods
|
||||
*
|
||||
* All add_*_field methods follow these common patterns:
|
||||
*
|
||||
* @param total_size Reference to the total message size to update
|
||||
* @param field_id_size Pre-calculated size of the field ID in bytes
|
||||
* @param value The value to calculate size for (type varies)
|
||||
* @param force Whether to calculate size even if the value is default/zero/empty
|
||||
*
|
||||
* Each method follows this implementation pattern:
|
||||
* 1. Skip calculation if value is default (0, false, empty) and not forced
|
||||
* 2. Calculate the size based on the field's encoding rules
|
||||
* 3. Add the field_id_size + calculated value size to total_size
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int32 field to the total message size
|
||||
*/
|
||||
static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
if (value < 0) {
|
||||
// Negative values are encoded as 10-byte varints in protobuf
|
||||
total_size += field_id_size + 10;
|
||||
} else {
|
||||
// For non-negative values, use the standard varint size
|
||||
total_size += field_id_size + varint(static_cast<uint32_t>(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint32 field to the total message size
|
||||
*/
|
||||
static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value,
|
||||
bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a boolean field to the total message size
|
||||
*/
|
||||
static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value, bool force = false) {
|
||||
// Skip calculation if value is false and not forced
|
||||
if (!value && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Boolean fields always use 1 byte when true
|
||||
total_size += field_id_size + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a fixed field to the total message size
|
||||
*
|
||||
* Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double).
|
||||
*
|
||||
* @tparam NumBytes The number of bytes for this fixed field (4 or 8)
|
||||
* @param is_nonzero Whether the value is non-zero
|
||||
*/
|
||||
template<uint32_t NumBytes>
|
||||
static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero,
|
||||
bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (!is_nonzero && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Fixed fields always take exactly NumBytes
|
||||
total_size += field_id_size + NumBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an enum field to the total message size
|
||||
*
|
||||
* Enum fields are encoded as uint32 varints.
|
||||
*/
|
||||
static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Enums are encoded as uint32
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sint32 field to the total message size
|
||||
*
|
||||
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
|
||||
*/
|
||||
static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
|
||||
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
||||
total_size += field_id_size + varint(zigzag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int64 field to the total message size
|
||||
*/
|
||||
static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint64 field to the total message size
|
||||
*/
|
||||
static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value,
|
||||
bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sint64 field to the total message size
|
||||
*
|
||||
* Sint64 fields use ZigZag encoding, which is more efficient for negative values.
|
||||
*/
|
||||
static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) {
|
||||
// Skip calculation if value is zero and not forced
|
||||
if (value == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
|
||||
uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
|
||||
total_size += field_id_size + varint(zigzag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a string/bytes field to the total message size
|
||||
*/
|
||||
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str,
|
||||
bool force = false) {
|
||||
// Skip calculation if string is empty and not forced
|
||||
if (str.empty() && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
const uint32_t str_size = static_cast<uint32_t>(str.size());
|
||||
total_size += field_id_size + varint(str_size) + str_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size
|
||||
*
|
||||
* This helper function directly updates the total_size reference if the nested size
|
||||
* is greater than zero or force is true.
|
||||
*
|
||||
* @param nested_size The pre-calculated size of the nested message
|
||||
*/
|
||||
static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size,
|
||||
bool force = false) {
|
||||
// Skip calculation if nested message is empty and not forced
|
||||
if (nested_size == 0 && !force) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
// Field ID + length varint + nested message content
|
||||
total_size += field_id_size + varint(nested_size) + nested_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size
|
||||
*
|
||||
* This templated version directly takes a message object, calculates its size internally,
|
||||
* and updates the total_size reference. This eliminates the need for a temporary variable
|
||||
* at the call site.
|
||||
*
|
||||
* @tparam MessageType The type of the nested message (inferred from parameter)
|
||||
* @param message The nested message object
|
||||
*/
|
||||
template<typename MessageType>
|
||||
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const MessageType &message,
|
||||
bool force = false) {
|
||||
uint32_t nested_size = 0;
|
||||
message.calculate_size(nested_size);
|
||||
|
||||
// Use the base implementation with the calculated nested_size
|
||||
add_message_field(total_size, field_id_size, nested_size, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size
|
||||
*
|
||||
* This helper processes a vector of message objects, calculating the size for each message
|
||||
* and adding it to the total size.
|
||||
*
|
||||
* @tparam MessageType The type of the nested messages in the vector
|
||||
* @param messages Vector of message objects
|
||||
*/
|
||||
template<typename MessageType>
|
||||
static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size,
|
||||
const std::vector<MessageType> &messages) {
|
||||
// Skip if the vector is empty
|
||||
if (messages.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For repeated fields, always use force=true
|
||||
for (const auto &message : messages) {
|
||||
add_message_object(total_size, field_id_size, message, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
@@ -31,6 +31,7 @@ APIServer::APIServer() {
|
||||
}
|
||||
|
||||
void APIServer::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
this->setup_controller();
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -46,11 +47,6 @@ void APIServer::setup() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Schedule reboot if no clients connect within timeout
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
this->schedule_reboot_timeout_();
|
||||
}
|
||||
|
||||
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
|
||||
if (this->socket_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not create socket");
|
||||
@@ -95,42 +91,34 @@ void APIServer::setup() {
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
if (logger::global_logger != nullptr) {
|
||||
logger::global_logger->add_on_log_callback(
|
||||
[this](int level, const char *tag, const char *message, size_t message_len) {
|
||||
if (this->shutting_down_) {
|
||||
// Don't try to send logs during shutdown
|
||||
// as it could result in a recursion and
|
||||
// we would be filling a buffer we are trying to clear
|
||||
return;
|
||||
}
|
||||
for (auto &c : this->clients_) {
|
||||
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
|
||||
c->try_send_log_message(level, tag, message, message_len);
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
|
||||
camera::Camera::instance()->add_image_callback([this](const std::shared_ptr<camera::CameraImage> &image) {
|
||||
logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
|
||||
if (this->shutting_down_) {
|
||||
// Don't try to send logs during shutdown
|
||||
// as it could result in a recursion and
|
||||
// we would be filling a buffer we are trying to clear
|
||||
return;
|
||||
}
|
||||
for (auto &c : this->clients_) {
|
||||
if (!c->flags_.remove)
|
||||
c->set_camera_state(image);
|
||||
if (!c->remove_)
|
||||
c->try_send_log_message(level, tag, message);
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIServer::schedule_reboot_timeout_() {
|
||||
this->status_set_warning();
|
||||
this->set_timeout("api_reboot", this->reboot_timeout_, []() {
|
||||
if (!global_api_server->is_connected()) {
|
||||
ESP_LOGE(TAG, "No clients; rebooting");
|
||||
App.reboot();
|
||||
}
|
||||
});
|
||||
this->last_connected_ = millis();
|
||||
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) {
|
||||
esp32_camera::global_esp32_camera->add_image_callback(
|
||||
[this](const std::shared_ptr<esp32_camera::CameraImage> &image) {
|
||||
for (auto &c : this->clients_) {
|
||||
if (!c->remove_)
|
||||
c->set_camera_state(image);
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIServer::loop() {
|
||||
@@ -142,82 +130,71 @@ void APIServer::loop() {
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
|
||||
ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear warning status and cancel reboot when first client connects
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->cancel_timeout("api_reboot");
|
||||
// Process clients and remove disconnected ones in a single pass
|
||||
if (!this->clients_.empty()) {
|
||||
size_t client_index = 0;
|
||||
while (client_index < this->clients_.size()) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
if (client->remove_) {
|
||||
// Handle disconnection
|
||||
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
|
||||
ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str());
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
} else {
|
||||
// Process active client
|
||||
client->loop();
|
||||
client_index++; // Move to next client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->clients_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process clients and remove disconnected ones in a single pass
|
||||
// Check network connectivity once for all clients
|
||||
if (!network::is_connected()) {
|
||||
// Network is down - disconnect all clients
|
||||
for (auto &client : this->clients_) {
|
||||
client->on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str());
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
const uint32_t now = millis();
|
||||
if (!this->is_connected()) {
|
||||
if (now - this->last_connected_ > this->reboot_timeout_) {
|
||||
ESP_LOGE(TAG, "No client connected; rebooting");
|
||||
App.reboot();
|
||||
}
|
||||
this->status_set_warning();
|
||||
} else {
|
||||
this->last_connected_ = now;
|
||||
this->status_clear_warning();
|
||||
}
|
||||
// Continue to process and clean up the clients below
|
||||
}
|
||||
|
||||
size_t client_index = 0;
|
||||
while (client_index < this->clients_.size()) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
if (!client->flags_.remove) {
|
||||
// Common case: process active client
|
||||
client->loop();
|
||||
client_index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str());
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Schedule reboot when last client disconnects
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->schedule_reboot_timeout_();
|
||||
}
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Server:\n"
|
||||
"API Server:\n"
|
||||
" Address: %s:%u",
|
||||
network::get_use_address().c_str(), this->port_);
|
||||
#ifdef USE_API_NOISE
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
|
||||
ESP_LOGCONFIG(TAG, " Using noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
|
||||
if (!this->noise_ctx_->has_psk()) {
|
||||
ESP_LOGCONFIG(TAG, " Supports encryption: YES");
|
||||
ESP_LOGCONFIG(TAG, " Supports noise encryption: YES");
|
||||
}
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: NO");
|
||||
ESP_LOGCONFIG(TAG, " Using 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();
|
||||
@@ -246,129 +223,192 @@ bool APIServer::check_password(const std::string &password) const {
|
||||
|
||||
return result == 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||
|
||||
// Macro for entities without extra parameters
|
||||
#define API_DISPATCH_UPDATE(entity_type, entity_name) \
|
||||
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
if (obj->is_internal()) \
|
||||
return; \
|
||||
for (auto &c : this->clients_) \
|
||||
c->send_##entity_name##_state(obj); \
|
||||
}
|
||||
|
||||
// Macro for entities with extra parameters (but parameters not used in send)
|
||||
#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
|
||||
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
if (obj->is_internal()) \
|
||||
return; \
|
||||
for (auto &c : this->clients_) \
|
||||
c->send_##entity_name##_state(obj); \
|
||||
}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
|
||||
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_binary_sensor_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_COVER
|
||||
API_DISPATCH_UPDATE(cover::Cover, cover)
|
||||
void APIServer::on_cover_update(cover::Cover *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_cover_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_FAN
|
||||
API_DISPATCH_UPDATE(fan::Fan, fan)
|
||||
void APIServer::on_fan_update(fan::Fan *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_fan_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
API_DISPATCH_UPDATE(light::LightState, light)
|
||||
void APIServer::on_light_update(light::LightState *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_light_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state)
|
||||
void APIServer::on_sensor_update(sensor::Sensor *obj, float state) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_sensor_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state)
|
||||
void APIServer::on_switch_update(switch_::Switch *obj, bool state) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_switch_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state)
|
||||
void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_text_sensor_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
API_DISPATCH_UPDATE(climate::Climate, climate)
|
||||
void APIServer::on_climate_update(climate::Climate *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_climate_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state)
|
||||
void APIServer::on_number_update(number::Number *obj, float state) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_number_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
API_DISPATCH_UPDATE(datetime::DateEntity, date)
|
||||
void APIServer::on_date_update(datetime::DateEntity *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_date_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
API_DISPATCH_UPDATE(datetime::TimeEntity, time)
|
||||
void APIServer::on_time_update(datetime::TimeEntity *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_time_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
|
||||
void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_datetime_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state)
|
||||
void APIServer::on_text_update(text::Text *obj, const std::string &state) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_text_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index)
|
||||
void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_select_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_LOCK
|
||||
API_DISPATCH_UPDATE(lock::Lock, lock)
|
||||
void APIServer::on_lock_update(lock::Lock *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_lock_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_VALVE
|
||||
API_DISPATCH_UPDATE(valve::Valve, valve)
|
||||
void APIServer::on_valve_update(valve::Valve *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_valve_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
|
||||
void APIServer::on_media_player_update(media_player::MediaPlayer *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_media_player_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
// Event is a special case - it's the only entity that passes extra parameters to the send method
|
||||
void APIServer::on_event(event::Event *obj, const std::string &event_type) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_event(obj, event_type);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
// Update is a special case - the method is called on_update, not on_update_update
|
||||
void APIServer::on_update(update::UpdateEntity *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_update_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
|
||||
void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_alarm_control_panel_state(obj);
|
||||
}
|
||||
#endif
|
||||
|
||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
#ifdef USE_API_PASSWORD
|
||||
void APIServer::set_password(const std::string &password) { this->password_ = password; }
|
||||
#endif
|
||||
|
||||
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||
void APIServer::set_batch_delay(uint32_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||
|
||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||
for (auto &client : this->clients_) {
|
||||
@@ -425,11 +465,10 @@ 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 PSK");
|
||||
ESP_LOGW(TAG, "Disconnecting all clients to reset connections");
|
||||
this->set_noise_psk(psk);
|
||||
for (auto &c : this->clients_) {
|
||||
DisconnectRequest req;
|
||||
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
|
||||
c->send_message(DisconnectRequest());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -440,7 +479,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void APIServer::request_time() {
|
||||
for (auto &client : this->clients_) {
|
||||
if (!client->flags_.remove && client->is_authenticated())
|
||||
if (!client->remove_ && client->is_authenticated())
|
||||
client->send_time_request();
|
||||
}
|
||||
}
|
||||
@@ -462,12 +501,10 @@ void APIServer::on_shutdown() {
|
||||
|
||||
// Send disconnect requests to all connected clients
|
||||
for (auto &c : this->clients_) {
|
||||
DisconnectRequest req;
|
||||
if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) {
|
||||
if (!c->send_message(DisconnectRequest())) {
|
||||
// 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,
|
||||
DisconnectRequest::ESTIMATED_SIZE);
|
||||
// schedule it in the batch so it will be sent with the 5ms timer
|
||||
c->schedule_message_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "list_entities.h"
|
||||
#include "subscribe_state.h"
|
||||
#ifdef USE_API_SERVICES
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
|
||||
#include <vector>
|
||||
|
||||
@@ -37,14 +35,13 @@ class APIServer : public Component, public Controller {
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
bool teardown() override;
|
||||
#ifdef USE_API_PASSWORD
|
||||
bool check_password(const std::string &password) const;
|
||||
void set_password(const std::string &password);
|
||||
#endif
|
||||
bool uses_password() const;
|
||||
void set_port(uint16_t port);
|
||||
void set_password(const std::string &password);
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
void set_batch_delay(uint16_t batch_delay);
|
||||
uint16_t get_batch_delay() const { return batch_delay_; }
|
||||
void set_batch_delay(uint32_t batch_delay);
|
||||
uint32_t get_batch_delay() const { return batch_delay_; }
|
||||
|
||||
// Get reference to shared buffer for API connections
|
||||
std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; }
|
||||
@@ -108,9 +105,7 @@ class APIServer : public Component, public Controller {
|
||||
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
||||
#endif
|
||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
||||
#ifdef USE_API_SERVICES
|
||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void request_time();
|
||||
#endif
|
||||
@@ -139,49 +134,27 @@ class APIServer : public Component, public Controller {
|
||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f);
|
||||
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
||||
#ifdef USE_API_SERVICES
|
||||
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> *get_client_disconnected_trigger() const {
|
||||
return this->client_disconnected_trigger_;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void schedule_reboot_timeout_();
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
bool shutting_down_ = false;
|
||||
std::unique_ptr<socket::Socket> socket_ = nullptr;
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>();
|
||||
#endif
|
||||
|
||||
// 4-byte aligned types
|
||||
uint16_t port_{6053};
|
||||
uint32_t reboot_timeout_{300000};
|
||||
|
||||
// Vectors and strings (12 bytes each on 32-bit)
|
||||
uint32_t batch_delay_{100};
|
||||
uint32_t last_connected_{0};
|
||||
std::vector<std::unique_ptr<APIConnection>> clients_;
|
||||
#ifdef USE_API_PASSWORD
|
||||
std::string password_;
|
||||
#endif
|
||||
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
|
||||
std::vector<HomeAssistantStateSubscription> state_subs_;
|
||||
#ifdef USE_API_SERVICES
|
||||
std::vector<UserServiceDescriptor *> user_services_;
|
||||
#endif
|
||||
|
||||
// Group smaller types together
|
||||
uint16_t port_{6053};
|
||||
uint16_t batch_delay_{100};
|
||||
bool shutting_down_ = false;
|
||||
// 5 bytes used, 3 bytes padding
|
||||
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
|
||||
Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>();
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
|
||||
|
||||
@@ -4,15 +4,9 @@ import asyncio
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import warnings
|
||||
|
||||
# Suppress protobuf version warnings
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings(
|
||||
"ignore", category=UserWarning, message=".*Protobuf gencode version.*"
|
||||
)
|
||||
from aioesphomeapi import APIClient, parse_log_message
|
||||
from aioesphomeapi.log_runner import async_run
|
||||
from aioesphomeapi import APIClient, parse_log_message
|
||||
from aioesphomeapi.log_runner import async_run
|
||||
|
||||
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
|
||||
from esphome.core import CORE
|
||||
@@ -35,8 +29,8 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
port: int = int(conf[CONF_PORT])
|
||||
password: str = conf[CONF_PASSWORD]
|
||||
noise_psk: str | None = None
|
||||
if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)):
|
||||
noise_psk = key
|
||||
if CONF_ENCRYPTION in conf:
|
||||
noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
|
||||
_LOGGER.info("Starting log output from %s using esphome API", address)
|
||||
cli = APIClient(
|
||||
address,
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
#include <map>
|
||||
#include "api_server.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_SERVICES
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
|
||||
public:
|
||||
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
|
||||
@@ -22,7 +19,6 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS
|
||||
T *obj_;
|
||||
void (T::*callback_)(Ts...);
|
||||
};
|
||||
#endif // USE_API_SERVICES
|
||||
|
||||
class CustomAPIDevice {
|
||||
public:
|
||||
@@ -50,14 +46,12 @@ class CustomAPIDevice {
|
||||
* @param name The name of the service to register.
|
||||
* @param arg_names The name of the arguments for the service, must match the arguments of the function.
|
||||
*/
|
||||
#ifdef USE_API_SERVICES
|
||||
template<typename T, typename... Ts>
|
||||
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
||||
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
||||
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
||||
global_api_server->register_user_service(service);
|
||||
}
|
||||
#endif
|
||||
|
||||
/** Register a custom native API service that will show up in Home Assistant.
|
||||
*
|
||||
@@ -77,12 +71,10 @@ class CustomAPIDevice {
|
||||
* @param callback The member function to call when the service is triggered.
|
||||
* @param name The name of the arguments for the service, must match the arguments of the function.
|
||||
*/
|
||||
#ifdef USE_API_SERVICES
|
||||
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
||||
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
||||
global_api_server->register_user_service(service);
|
||||
}
|
||||
#endif
|
||||
|
||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
|
||||
*
|
||||
|
||||
@@ -11,18 +11,6 @@ namespace esphome {
|
||||
namespace api {
|
||||
|
||||
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
|
||||
private:
|
||||
// Helper to convert value to string - handles the case where value is already a string
|
||||
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
||||
|
||||
// Overloads for string types - needed because std::to_string doesn't support them
|
||||
static std::string value_to_string(char *val) {
|
||||
return val ? std::string(val) : std::string();
|
||||
} // For lambdas returning char* (e.g., itoa)
|
||||
static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str()
|
||||
static std::string value_to_string(const std::string &val) { return val; }
|
||||
static std::string value_to_string(std::string &&val) { return std::move(val); }
|
||||
|
||||
public:
|
||||
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
|
||||
|
||||
@@ -31,7 +19,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
||||
|
||||
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
|
||||
TemplatableStringValue(F f)
|
||||
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return value_to_string(f(x...)); }) {}
|
||||
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {}
|
||||
};
|
||||
|
||||
template<typename... Ts> class TemplatableKeyValuePair {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "list_entities.h"
|
||||
#ifdef USE_API
|
||||
#include "api_connection.h"
|
||||
#include "api_pb2.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
@@ -9,84 +8,152 @@
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
|
||||
// Generate entity handler implementations using macros
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LIST_ENTITIES_HANDLER(binary_sensor, binary_sensor::BinarySensor, ListEntitiesBinarySensorResponse)
|
||||
bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
|
||||
this->client_->send_binary_sensor_info(binary_sensor);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
LIST_ENTITIES_HANDLER(cover, cover::Cover, ListEntitiesCoverResponse)
|
||||
bool ListEntitiesIterator::on_cover(cover::Cover *cover) {
|
||||
this->client_->send_cover_info(cover);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
LIST_ENTITIES_HANDLER(fan, fan::Fan, ListEntitiesFanResponse)
|
||||
bool ListEntitiesIterator::on_fan(fan::Fan *fan) {
|
||||
this->client_->send_fan_info(fan);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
LIST_ENTITIES_HANDLER(light, light::LightState, ListEntitiesLightResponse)
|
||||
bool ListEntitiesIterator::on_light(light::LightState *light) {
|
||||
this->client_->send_light_info(light);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
LIST_ENTITIES_HANDLER(sensor, sensor::Sensor, ListEntitiesSensorResponse)
|
||||
bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) {
|
||||
this->client_->send_sensor_info(sensor);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
LIST_ENTITIES_HANDLER(switch, switch_::Switch, ListEntitiesSwitchResponse)
|
||||
bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) {
|
||||
this->client_->send_switch_info(a_switch);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
LIST_ENTITIES_HANDLER(button, button::Button, ListEntitiesButtonResponse)
|
||||
bool ListEntitiesIterator::on_button(button::Button *button) {
|
||||
this->client_->send_button_info(button);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
LIST_ENTITIES_HANDLER(text_sensor, text_sensor::TextSensor, ListEntitiesTextSensorResponse)
|
||||
bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) {
|
||||
this->client_->send_text_sensor_info(text_sensor);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
LIST_ENTITIES_HANDLER(lock, lock::Lock, ListEntitiesLockResponse)
|
||||
bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) {
|
||||
this->client_->send_lock_info(a_lock);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
LIST_ENTITIES_HANDLER(valve, valve::Valve, ListEntitiesValveResponse)
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
LIST_ENTITIES_HANDLER(camera, camera::Camera, ListEntitiesCameraResponse)
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
LIST_ENTITIES_HANDLER(climate, climate::Climate, ListEntitiesClimateResponse)
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
LIST_ENTITIES_HANDLER(number, number::Number, ListEntitiesNumberResponse)
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
LIST_ENTITIES_HANDLER(date, datetime::DateEntity, ListEntitiesDateResponse)
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
LIST_ENTITIES_HANDLER(time, datetime::TimeEntity, ListEntitiesTimeResponse)
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
LIST_ENTITIES_HANDLER(datetime, datetime::DateTimeEntity, ListEntitiesDateTimeResponse)
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
LIST_ENTITIES_HANDLER(text, text::Text, ListEntitiesTextResponse)
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
LIST_ENTITIES_HANDLER(select, select::Select, ListEntitiesSelectResponse)
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMediaPlayerResponse)
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel,
|
||||
ListEntitiesAlarmControlPanelResponse)
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
LIST_ENTITIES_HANDLER(update, update::UpdateEntity, ListEntitiesUpdateResponse)
|
||||
bool ListEntitiesIterator::on_valve(valve::Valve *valve) {
|
||||
this->client_->send_valve_info(valve);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Special cases that don't follow the pattern
|
||||
bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); }
|
||||
|
||||
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||
auto resp = service->encode_list_service_response();
|
||||
return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE);
|
||||
return this->client_->send_message(resp);
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) {
|
||||
this->client_->send_camera_info(camera);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
bool ListEntitiesIterator::on_climate(climate::Climate *climate) {
|
||||
this->client_->send_climate_info(climate);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
bool ListEntitiesIterator::on_number(number::Number *number) {
|
||||
this->client_->send_number_info(number);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
bool ListEntitiesIterator::on_date(datetime::DateEntity *date) {
|
||||
this->client_->send_date_info(date);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) {
|
||||
this->client_->send_time_info(time);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) {
|
||||
this->client_->send_datetime_info(datetime);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
bool ListEntitiesIterator::on_text(text::Text *text) {
|
||||
this->client_->send_text_info(text);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
bool ListEntitiesIterator::on_select(select::Select *select) {
|
||||
this->client_->send_select_info(select);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) {
|
||||
this->client_->send_media_player_info(media_player);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
|
||||
this->client_->send_alarm_control_panel_info(a_alarm_control_panel);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool ListEntitiesIterator::on_event(event::Event *event) {
|
||||
this->client_->send_event_info(event);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool ListEntitiesIterator::on_update(update::UpdateEntity *update) {
|
||||
this->client_->send_update_info(update);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -9,85 +9,75 @@ namespace api {
|
||||
|
||||
class APIConnection;
|
||||
|
||||
// Macro for generating ListEntitiesIterator handlers
|
||||
// Calls schedule_message_ with try_send_*_info
|
||||
#define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \
|
||||
bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \
|
||||
ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \
|
||||
}
|
||||
|
||||
class ListEntitiesIterator : public ComponentIterator {
|
||||
public:
|
||||
ListEntitiesIterator(APIConnection *client);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
bool on_binary_sensor(binary_sensor::BinarySensor *entity) override;
|
||||
bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
bool on_cover(cover::Cover *entity) override;
|
||||
bool on_cover(cover::Cover *cover) override;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
bool on_fan(fan::Fan *entity) override;
|
||||
bool on_fan(fan::Fan *fan) override;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
bool on_light(light::LightState *entity) override;
|
||||
bool on_light(light::LightState *light) override;
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
bool on_sensor(sensor::Sensor *entity) override;
|
||||
bool on_sensor(sensor::Sensor *sensor) override;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
bool on_switch(switch_::Switch *entity) override;
|
||||
bool on_switch(switch_::Switch *a_switch) override;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
bool on_button(button::Button *entity) override;
|
||||
bool on_button(button::Button *button) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool on_text_sensor(text_sensor::TextSensor *entity) override;
|
||||
bool on_text_sensor(text_sensor::TextSensor *text_sensor) override;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
bool on_service(UserServiceDescriptor *service) override;
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
bool on_camera(camera::Camera *entity) override;
|
||||
#ifdef USE_ESP32_CAMERA
|
||||
bool on_camera(esp32_camera::ESP32Camera *camera) override;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
bool on_climate(climate::Climate *entity) override;
|
||||
bool on_climate(climate::Climate *climate) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
bool on_number(number::Number *entity) override;
|
||||
bool on_number(number::Number *number) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
bool on_date(datetime::DateEntity *entity) override;
|
||||
bool on_date(datetime::DateEntity *date) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
bool on_time(datetime::TimeEntity *entity) override;
|
||||
bool on_time(datetime::TimeEntity *time) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
bool on_datetime(datetime::DateTimeEntity *entity) override;
|
||||
bool on_datetime(datetime::DateTimeEntity *datetime) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool on_text(text::Text *entity) override;
|
||||
bool on_text(text::Text *text) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool on_select(select::Select *entity) override;
|
||||
bool on_select(select::Select *select) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool on_lock(lock::Lock *entity) override;
|
||||
bool on_lock(lock::Lock *a_lock) override;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
bool on_valve(valve::Valve *entity) override;
|
||||
bool on_valve(valve::Valve *valve) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool on_media_player(media_player::MediaPlayer *entity) override;
|
||||
bool on_media_player(media_player::MediaPlayer *media_player) override;
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
|
||||
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override;
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *entity) override;
|
||||
bool on_event(event::Event *event) override;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool on_update(update::UpdateEntity *entity) override;
|
||||
bool on_update(update::UpdateEntity *update) override;
|
||||
#endif
|
||||
bool on_end() override;
|
||||
bool completed() { return this->state_ == IteratorState::NONE; }
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace api {
|
||||
|
||||
static const char *const TAG = "api.proto";
|
||||
|
||||
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
void ProtoMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
uint32_t i = 0;
|
||||
bool error = false;
|
||||
while (i < length) {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
@@ -60,6 +59,7 @@ class ProtoVarInt {
|
||||
uint32_t as_uint32() const { return this->value_; }
|
||||
uint64_t as_uint64() const { return this->value_; }
|
||||
bool as_bool() const { return this->value_; }
|
||||
template<typename T> T as_enum() const { return static_cast<T>(this->as_uint32()); }
|
||||
int32_t as_int32() const {
|
||||
// Not ZigZag encoded
|
||||
return static_cast<int32_t>(this->as_int64());
|
||||
@@ -133,25 +133,15 @@ class ProtoVarInt {
|
||||
uint64_t value_;
|
||||
};
|
||||
|
||||
// Forward declaration for decode_to_message and encode_to_writer
|
||||
class ProtoMessage;
|
||||
class ProtoDecodableMessage;
|
||||
|
||||
class ProtoLengthDelimited {
|
||||
public:
|
||||
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
|
||||
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
|
||||
|
||||
/**
|
||||
* Decode the length-delimited data into an existing ProtoDecodableMessage instance.
|
||||
*
|
||||
* This method allows decoding without templates, enabling use in contexts
|
||||
* where the message type is not known at compile time. The ProtoDecodableMessage's
|
||||
* decode() method will be called with the raw data and length.
|
||||
*
|
||||
* @param msg The ProtoDecodableMessage instance to decode into
|
||||
*/
|
||||
void decode_to_message(ProtoDecodableMessage &msg) const;
|
||||
template<class C> C as_message() const {
|
||||
auto msg = C();
|
||||
msg.decode(this->value_, this->length_);
|
||||
return msg;
|
||||
}
|
||||
|
||||
protected:
|
||||
const uint8_t *const value_;
|
||||
@@ -176,7 +166,23 @@ class Proto32Bit {
|
||||
const uint32_t value_;
|
||||
};
|
||||
|
||||
// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
|
||||
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<int64_t>(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_;
|
||||
};
|
||||
|
||||
class ProtoWriteBuffer {
|
||||
public:
|
||||
@@ -190,9 +196,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
|
||||
*/
|
||||
@@ -210,7 +216,7 @@ class ProtoWriteBuffer {
|
||||
this->buffer_->insert(this->buffer_->end(), data, data + 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);
|
||||
this->encode_string(field_id, value.data(), value.size());
|
||||
}
|
||||
void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) {
|
||||
this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force);
|
||||
@@ -243,10 +249,23 @@ class ProtoWriteBuffer {
|
||||
this->write((value >> 16) & 0xFF);
|
||||
this->write((value >> 24) & 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_fixed64(uint32_t field_id, uint64_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
|
||||
this->encode_field_raw(field_id, 1); // type 1: 64-bit fixed64
|
||||
this->write((value >> 0) & 0xFF);
|
||||
this->write((value >> 8) & 0xFF);
|
||||
this->write((value >> 16) & 0xFF);
|
||||
this->write((value >> 24) & 0xFF);
|
||||
this->write((value >> 32) & 0xFF);
|
||||
this->write((value >> 40) & 0xFF);
|
||||
this->write((value >> 48) & 0xFF);
|
||||
this->write((value >> 56) & 0xFF);
|
||||
}
|
||||
template<typename T> void encode_enum(uint32_t field_id, T value, bool force = false) {
|
||||
this->encode_uint32(field_id, static_cast<uint32_t>(value), force);
|
||||
}
|
||||
void encode_float(uint32_t field_id, float value, bool force = false) {
|
||||
if (value == 0.0f && !force)
|
||||
return;
|
||||
@@ -287,7 +306,18 @@ class ProtoWriteBuffer {
|
||||
}
|
||||
this->encode_uint64(field_id, uvalue, force);
|
||||
}
|
||||
void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false);
|
||||
template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) {
|
||||
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
|
||||
size_t begin = this->buffer_->size();
|
||||
|
||||
value.encode(*this);
|
||||
|
||||
const uint32_t nested_length = this->buffer_->size() - begin;
|
||||
// add size varint
|
||||
std::vector<uint8_t> var;
|
||||
ProtoVarInt(nested_length).encode(var);
|
||||
this->buffer_->insert(this->buffer_->begin() + begin, var.begin(), var.end());
|
||||
}
|
||||
std::vector<uint8_t> *get_buffer() const { return buffer_; }
|
||||
|
||||
protected:
|
||||
@@ -297,528 +327,21 @@ class ProtoWriteBuffer {
|
||||
class ProtoMessage {
|
||||
public:
|
||||
virtual ~ProtoMessage() = default;
|
||||
// Default implementation for messages with no fields
|
||||
virtual void encode(ProtoWriteBuffer buffer) const {}
|
||||
// Default implementation for messages with no fields
|
||||
virtual void calculate_size(uint32_t &total_size) const {}
|
||||
virtual void encode(ProtoWriteBuffer buffer) const = 0;
|
||||
void decode(const uint8_t *buffer, size_t length);
|
||||
virtual void calculate_size(uint32_t &total_size) const = 0;
|
||||
#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; }
|
||||
// NOTE: decode_64bit removed - wire type 1 not supported
|
||||
virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; }
|
||||
};
|
||||
|
||||
class ProtoSize {
|
||||
public:
|
||||
/**
|
||||
* @brief ProtoSize class for Protocol Buffer serialization size calculation
|
||||
*
|
||||
* This class provides static methods to calculate the exact byte counts needed
|
||||
* for encoding various Protocol Buffer field types. All methods are designed to be
|
||||
* efficient for the common case where many fields have default values.
|
||||
*
|
||||
* Implements Protocol Buffer encoding size calculation according to:
|
||||
* https://protobuf.dev/programming-guides/encoding/
|
||||
*
|
||||
* Key features:
|
||||
* - Early-return optimization for zero/default values
|
||||
* - Direct total_size updates to avoid unnecessary additions
|
||||
* - Specialized handling for different field types according to protobuf spec
|
||||
* - Templated helpers for repeated fields and messages
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
||||
*
|
||||
* @param value The uint32_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(uint32_t value) {
|
||||
// Optimized varint size calculation using leading zeros
|
||||
// Each 7 bits requires one byte in the varint encoding
|
||||
if (value < 128)
|
||||
return 1; // 7 bits, common case for small values
|
||||
|
||||
// For larger values, count bytes needed based on the position of the highest bit set
|
||||
if (value < 16384) {
|
||||
return 2; // 14 bits
|
||||
} else if (value < 2097152) {
|
||||
return 3; // 21 bits
|
||||
} else if (value < 268435456) {
|
||||
return 4; // 28 bits
|
||||
} else {
|
||||
return 5; // 32 bits (maximum for uint32_t)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint64_t value as a varint
|
||||
*
|
||||
* @param value The uint64_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(uint64_t value) {
|
||||
// Handle common case of values fitting in uint32_t (vast majority of use cases)
|
||||
if (value <= UINT32_MAX) {
|
||||
return varint(static_cast<uint32_t>(value));
|
||||
}
|
||||
|
||||
// For larger values, determine size based on highest bit position
|
||||
if (value < (1ULL << 35)) {
|
||||
return 5; // 35 bits
|
||||
} else if (value < (1ULL << 42)) {
|
||||
return 6; // 42 bits
|
||||
} else if (value < (1ULL << 49)) {
|
||||
return 7; // 49 bits
|
||||
} else if (value < (1ULL << 56)) {
|
||||
return 8; // 56 bits
|
||||
} else if (value < (1ULL << 63)) {
|
||||
return 9; // 63 bits
|
||||
} else {
|
||||
return 10; // 64 bits (maximum for uint64_t)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode an int32_t value as a varint
|
||||
*
|
||||
* Special handling is needed for negative values, which are sign-extended to 64 bits
|
||||
* in Protocol Buffers, resulting in a 10-byte varint.
|
||||
*
|
||||
* @param value The int32_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(int32_t value) {
|
||||
// Negative values are sign-extended to 64 bits in protocol buffers,
|
||||
// which always results in a 10-byte varint for negative int32
|
||||
if (value < 0) {
|
||||
return 10; // Negative int32 is always 10 bytes long
|
||||
}
|
||||
// For non-negative values, use the uint32_t implementation
|
||||
return varint(static_cast<uint32_t>(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode an int64_t value as a varint
|
||||
*
|
||||
* @param value The int64_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(int64_t value) {
|
||||
// For int64_t, we convert to uint64_t and calculate the size
|
||||
// This works because the bit pattern determines the encoding size,
|
||||
// and we've handled negative int32 values as a special case above
|
||||
return varint(static_cast<uint64_t>(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a field ID and wire type
|
||||
*
|
||||
* @param field_id The field identifier
|
||||
* @param type The wire type value (from the WireType enum in the protobuf spec)
|
||||
* @return The number of bytes needed to encode the field ID and wire type
|
||||
*/
|
||||
static inline uint32_t field(uint32_t field_id, uint32_t type) {
|
||||
uint32_t tag = (field_id << 3) | (type & 0b111);
|
||||
return varint(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Common parameters for all add_*_field methods
|
||||
*
|
||||
* All add_*_field methods follow these common patterns:
|
||||
*
|
||||
* @param total_size Reference to the total message size to update
|
||||
* @param field_id_size Pre-calculated size of the field ID in bytes
|
||||
* @param value The value to calculate size for (type varies)
|
||||
* @param force Whether to calculate size even if the value is default/zero/empty
|
||||
*
|
||||
* Each method follows this implementation pattern:
|
||||
* 1. Skip calculation if value is default (0, false, empty) and not forced
|
||||
* 2. Calculate the size based on the field's encoding rules
|
||||
* 3. Add the field_id_size + calculated value size to total_size
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int32 field to the total message size
|
||||
*/
|
||||
static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
|
||||
// Skip calculation if value is zero
|
||||
if (value == 0) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
if (value < 0) {
|
||||
// Negative values are encoded as 10-byte varints in protobuf
|
||||
total_size += field_id_size + 10;
|
||||
} else {
|
||||
// For non-negative values, use the standard varint size
|
||||
total_size += field_id_size + varint(static_cast<uint32_t>(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int32 field to the total message size (repeated field version)
|
||||
*/
|
||||
static inline void add_int32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
|
||||
// Always calculate size for repeated fields
|
||||
if (value < 0) {
|
||||
// Negative values are encoded as 10-byte varints in protobuf
|
||||
total_size += field_id_size + 10;
|
||||
} else {
|
||||
// For non-negative values, use the standard varint size
|
||||
total_size += field_id_size + varint(static_cast<uint32_t>(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint32 field to the total message size
|
||||
*/
|
||||
static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
|
||||
// Skip calculation if value is zero
|
||||
if (value == 0) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint32 field to the total message size (repeated field version)
|
||||
*/
|
||||
static inline void add_uint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
|
||||
// Always calculate size for repeated fields
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a boolean field to the total message size
|
||||
*/
|
||||
static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value) {
|
||||
// Skip calculation if value is false
|
||||
if (!value) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Boolean fields always use 1 byte when true
|
||||
total_size += field_id_size + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a boolean field to the total message size (repeated field version)
|
||||
*/
|
||||
static inline void add_bool_field_repeated(uint32_t &total_size, uint32_t field_id_size, bool value) {
|
||||
// Always calculate size for repeated fields
|
||||
// Boolean fields always use 1 byte
|
||||
total_size += field_id_size + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a fixed field to the total message size
|
||||
*
|
||||
* Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double).
|
||||
*
|
||||
* @tparam NumBytes The number of bytes for this fixed field (4 or 8)
|
||||
* @param is_nonzero Whether the value is non-zero
|
||||
*/
|
||||
template<uint32_t NumBytes>
|
||||
static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero) {
|
||||
// Skip calculation if value is zero
|
||||
if (!is_nonzero) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Fixed fields always take exactly NumBytes
|
||||
total_size += field_id_size + NumBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a float field to the total message size
|
||||
*/
|
||||
static inline void add_float_field(uint32_t &total_size, uint32_t field_id_size, float value) {
|
||||
if (value != 0.0f) {
|
||||
total_size += field_id_size + 4;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: add_double_field removed - wire type 1 (64-bit: double) not supported
|
||||
// to reduce overhead on embedded systems
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a fixed32 field to the total message size
|
||||
*/
|
||||
static inline void add_fixed32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
|
||||
if (value != 0) {
|
||||
total_size += field_id_size + 4;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: add_fixed64_field removed - wire type 1 (64-bit: fixed64) not supported
|
||||
// to reduce overhead on embedded systems
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sfixed32 field to the total message size
|
||||
*/
|
||||
static inline void add_sfixed32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
|
||||
if (value != 0) {
|
||||
total_size += field_id_size + 4;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported
|
||||
// to reduce overhead on embedded systems
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an enum field to the total message size
|
||||
*
|
||||
* Enum fields are encoded as uint32 varints.
|
||||
*/
|
||||
static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
|
||||
// Skip calculation if value is zero
|
||||
if (value == 0) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Enums are encoded as uint32
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an enum field to the total message size (repeated field version)
|
||||
*
|
||||
* Enum fields are encoded as uint32 varints.
|
||||
*/
|
||||
static inline void add_enum_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
|
||||
// Always calculate size for repeated fields
|
||||
// Enums are encoded as uint32
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sint32 field to the total message size
|
||||
*
|
||||
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
|
||||
*/
|
||||
static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
|
||||
// Skip calculation if value is zero
|
||||
if (value == 0) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
|
||||
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
||||
total_size += field_id_size + varint(zigzag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sint32 field to the total message size (repeated field version)
|
||||
*
|
||||
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
|
||||
*/
|
||||
static inline void add_sint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
|
||||
// Always calculate size for repeated fields
|
||||
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
|
||||
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
||||
total_size += field_id_size + varint(zigzag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int64 field to the total message size
|
||||
*/
|
||||
static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
|
||||
// Skip calculation if value is zero
|
||||
if (value == 0) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int64 field to the total message size (repeated field version)
|
||||
*/
|
||||
static inline void add_int64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
|
||||
// Always calculate size for repeated fields
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint64 field to the total message size
|
||||
*/
|
||||
static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value) {
|
||||
// Skip calculation if value is zero
|
||||
if (value == 0) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint64 field to the total message size (repeated field version)
|
||||
*/
|
||||
static inline void add_uint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint64_t value) {
|
||||
// Always calculate size for repeated fields
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
// NOTE: sint64 support functions (add_sint64_field, add_sint64_field_repeated) removed
|
||||
// sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a string/bytes field to the total message size
|
||||
*/
|
||||
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str) {
|
||||
// Skip calculation if string is empty
|
||||
if (str.empty()) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
const uint32_t str_size = static_cast<uint32_t>(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<uint32_t>(str.size());
|
||||
total_size += field_id_size + varint(str_size) + str_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size
|
||||
*
|
||||
* This helper function directly updates the total_size reference if the nested size
|
||||
* is greater than zero.
|
||||
*
|
||||
* @param nested_size The pre-calculated size of the nested message
|
||||
*/
|
||||
static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) {
|
||||
// Skip calculation if nested message is empty
|
||||
if (nested_size == 0) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
// Field ID + length varint + nested message content
|
||||
total_size += field_id_size + varint(nested_size) + nested_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size (repeated field version)
|
||||
*
|
||||
* @param nested_size The pre-calculated size of the nested message
|
||||
*/
|
||||
static inline void add_message_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) {
|
||||
// Always calculate size for repeated fields
|
||||
// Field ID + length varint + nested message content
|
||||
total_size += field_id_size + varint(nested_size) + nested_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size
|
||||
*
|
||||
* This version takes a ProtoMessage object, calculates its size internally,
|
||||
* and updates the total_size reference. This eliminates the need for a temporary variable
|
||||
* at the call site.
|
||||
*
|
||||
* @param message The nested message object
|
||||
*/
|
||||
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message) {
|
||||
uint32_t nested_size = 0;
|
||||
message.calculate_size(nested_size);
|
||||
|
||||
// Use the base implementation with the calculated nested_size
|
||||
add_message_field(total_size, field_id_size, nested_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size (repeated field version)
|
||||
*
|
||||
* @param message The nested message object
|
||||
*/
|
||||
static inline void add_message_object_repeated(uint32_t &total_size, uint32_t field_id_size,
|
||||
const ProtoMessage &message) {
|
||||
uint32_t nested_size = 0;
|
||||
message.calculate_size(nested_size);
|
||||
|
||||
// Use the base implementation with the calculated nested_size
|
||||
add_message_field_repeated(total_size, field_id_size, nested_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size
|
||||
*
|
||||
* This helper processes a vector of message objects, calculating the size for each message
|
||||
* and adding it to the total size.
|
||||
*
|
||||
* @tparam MessageType The type of the nested messages in the vector
|
||||
* @param messages Vector of message objects
|
||||
*/
|
||||
template<typename MessageType>
|
||||
static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size,
|
||||
const std::vector<MessageType> &messages) {
|
||||
// Skip if the vector is empty
|
||||
if (messages.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the repeated field version for all messages
|
||||
for (const auto &message : messages) {
|
||||
add_message_object_repeated(total_size, field_id_size, message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Implementation of encode_message - must be after ProtoMessage is defined
|
||||
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) {
|
||||
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
|
||||
|
||||
// Calculate the message size first
|
||||
uint32_t msg_length_bytes = 0;
|
||||
value.calculate_size(msg_length_bytes);
|
||||
|
||||
// Calculate how many bytes the length varint needs
|
||||
uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes);
|
||||
|
||||
// Reserve exact space for the length varint
|
||||
size_t begin = this->buffer_->size();
|
||||
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
|
||||
|
||||
// Write the length varint directly
|
||||
ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes);
|
||||
|
||||
// Now encode the message content - it will append to the buffer
|
||||
value.encode(*this);
|
||||
|
||||
// Verify that the encoded size matches what we calculated
|
||||
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
|
||||
}
|
||||
|
||||
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
|
||||
inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) const {
|
||||
msg.decode(this->value_, this->length_);
|
||||
}
|
||||
|
||||
template<typename T> const char *proto_enum_to_string(T value);
|
||||
|
||||
class ProtoService {
|
||||
@@ -837,11 +360,11 @@ class ProtoService {
|
||||
* @return A ProtoWriteBuffer object with the reserved size.
|
||||
*/
|
||||
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
|
||||
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
|
||||
virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
|
||||
virtual bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) = 0;
|
||||
virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
|
||||
|
||||
// Optimized method that pre-allocates buffer based on message size
|
||||
bool send_message_(const ProtoMessage &msg, uint8_t message_type) {
|
||||
bool send_message_(const ProtoMessage &msg, uint16_t message_type) {
|
||||
uint32_t msg_size = 0;
|
||||
msg.calculate_size(msg_size);
|
||||
|
||||
@@ -854,26 +377,6 @@ class ProtoService {
|
||||
// Send the buffer
|
||||
return this->send_buffer(buffer, message_type);
|
||||
}
|
||||
|
||||
// Authentication helper methods
|
||||
bool check_connection_setup_() {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool check_authenticated_() {
|
||||
if (!this->check_connection_setup_()) {
|
||||
return false;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
|
||||
@@ -6,67 +6,73 @@
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
|
||||
// Generate entity handler implementations using macros
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
INITIAL_STATE_HANDLER(binary_sensor, binary_sensor::BinarySensor)
|
||||
bool InitialStateIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
|
||||
return this->client_->send_binary_sensor_state(binary_sensor);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
INITIAL_STATE_HANDLER(cover, cover::Cover)
|
||||
bool InitialStateIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_state(cover); }
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
INITIAL_STATE_HANDLER(fan, fan::Fan)
|
||||
bool InitialStateIterator::on_fan(fan::Fan *fan) { return this->client_->send_fan_state(fan); }
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
INITIAL_STATE_HANDLER(light, light::LightState)
|
||||
bool InitialStateIterator::on_light(light::LightState *light) { return this->client_->send_light_state(light); }
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
INITIAL_STATE_HANDLER(sensor, sensor::Sensor)
|
||||
bool InitialStateIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_state(sensor); }
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
INITIAL_STATE_HANDLER(switch, switch_::Switch)
|
||||
bool InitialStateIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_state(a_switch); }
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
INITIAL_STATE_HANDLER(text_sensor, text_sensor::TextSensor)
|
||||
bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) {
|
||||
return this->client_->send_text_sensor_state(text_sensor);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
INITIAL_STATE_HANDLER(climate, climate::Climate)
|
||||
bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); }
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
INITIAL_STATE_HANDLER(number, number::Number)
|
||||
bool InitialStateIterator::on_number(number::Number *number) { return this->client_->send_number_state(number); }
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
INITIAL_STATE_HANDLER(date, datetime::DateEntity)
|
||||
bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_state(date); }
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
INITIAL_STATE_HANDLER(time, datetime::TimeEntity)
|
||||
bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); }
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
INITIAL_STATE_HANDLER(datetime, datetime::DateTimeEntity)
|
||||
bool InitialStateIterator::on_datetime(datetime::DateTimeEntity *datetime) {
|
||||
return this->client_->send_datetime_state(datetime);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
INITIAL_STATE_HANDLER(text, text::Text)
|
||||
bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text); }
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
INITIAL_STATE_HANDLER(select, select::Select)
|
||||
bool InitialStateIterator::on_select(select::Select *select) { return this->client_->send_select_state(select); }
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
INITIAL_STATE_HANDLER(lock, lock::Lock)
|
||||
bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock); }
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
INITIAL_STATE_HANDLER(valve, valve::Valve)
|
||||
bool InitialStateIterator::on_valve(valve::Valve *valve) { return this->client_->send_valve_state(valve); }
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer)
|
||||
bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) {
|
||||
return this->client_->send_media_player_state(media_player);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel)
|
||||
bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
|
||||
return this->client_->send_alarm_control_panel_state(a_alarm_control_panel);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
INITIAL_STATE_HANDLER(update, update::UpdateEntity)
|
||||
bool InitialStateIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_state(update); }
|
||||
#endif
|
||||
|
||||
// Special cases (button and event) are already defined inline in subscribe_state.h
|
||||
|
||||
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
|
||||
|
||||
} // namespace api
|
||||
|
||||
@@ -10,78 +10,71 @@ namespace api {
|
||||
|
||||
class APIConnection;
|
||||
|
||||
// Macro for generating InitialStateIterator handlers
|
||||
// Calls send_*_state
|
||||
#define INITIAL_STATE_HANDLER(entity_type, EntityClass) \
|
||||
bool InitialStateIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
return this->client_->send_##entity_type##_state(entity); \
|
||||
}
|
||||
|
||||
class InitialStateIterator : public ComponentIterator {
|
||||
public:
|
||||
InitialStateIterator(APIConnection *client);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
bool on_binary_sensor(binary_sensor::BinarySensor *entity) override;
|
||||
bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
bool on_cover(cover::Cover *entity) override;
|
||||
bool on_cover(cover::Cover *cover) override;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
bool on_fan(fan::Fan *entity) override;
|
||||
bool on_fan(fan::Fan *fan) override;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
bool on_light(light::LightState *entity) override;
|
||||
bool on_light(light::LightState *light) override;
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
bool on_sensor(sensor::Sensor *entity) override;
|
||||
bool on_sensor(sensor::Sensor *sensor) override;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
bool on_switch(switch_::Switch *entity) override;
|
||||
bool on_switch(switch_::Switch *a_switch) override;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
bool on_button(button::Button *button) override { return true; };
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool on_text_sensor(text_sensor::TextSensor *entity) override;
|
||||
bool on_text_sensor(text_sensor::TextSensor *text_sensor) override;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
bool on_climate(climate::Climate *entity) override;
|
||||
bool on_climate(climate::Climate *climate) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
bool on_number(number::Number *entity) override;
|
||||
bool on_number(number::Number *number) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
bool on_date(datetime::DateEntity *entity) override;
|
||||
bool on_date(datetime::DateEntity *date) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
bool on_time(datetime::TimeEntity *entity) override;
|
||||
bool on_time(datetime::TimeEntity *time) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
bool on_datetime(datetime::DateTimeEntity *entity) override;
|
||||
bool on_datetime(datetime::DateTimeEntity *datetime) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool on_text(text::Text *entity) override;
|
||||
bool on_text(text::Text *text) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool on_select(select::Select *entity) override;
|
||||
bool on_select(select::Select *select) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool on_lock(lock::Lock *entity) override;
|
||||
bool on_lock(lock::Lock *a_lock) override;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
bool on_valve(valve::Valve *entity) override;
|
||||
bool on_valve(valve::Valve *valve) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool on_media_player(media_player::MediaPlayer *entity) override;
|
||||
bool on_media_player(media_player::MediaPlayer *media_player) override;
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
|
||||
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override;
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *event) override { return true; };
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
bool on_update(update::UpdateEntity *entity) override;
|
||||
bool on_update(update::UpdateEntity *update) override;
|
||||
#endif
|
||||
bool completed() { return this->state_ == IteratorState::NONE; }
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "api_pb2.h"
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
|
||||
@@ -16,8 +15,6 @@ class UserServiceDescriptor {
|
||||
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
|
||||
|
||||
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
|
||||
|
||||
bool is_internal() { return false; }
|
||||
};
|
||||
|
||||
template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
|
||||
@@ -76,4 +73,3 @@ template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
#endif // USE_API_SERVICES
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/as3935/as3935.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace as3935_spi {
|
||||
|
||||
@@ -50,6 +50,7 @@ class AS5600Component : public Component, public i2c::I2CDevice {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
/// HARDWARE_LATE setup priority
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
// configuration setters
|
||||
void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; }
|
||||
|
||||
@@ -5,7 +5,6 @@ from esphome.const import (
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
@@ -15,23 +14,15 @@ CODEOWNERS = ["@OttoWinter"]
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema({}),
|
||||
cv.only_with_arduino,
|
||||
cv.only_on(
|
||||
[
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
]
|
||||
),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(200.0)
|
||||
async def to_code(config):
|
||||
if CORE.is_esp32 or CORE.is_libretiny:
|
||||
# https://github.com/ESP32Async/AsyncTCP
|
||||
cg.add_library("ESP32Async/AsyncTCP", "3.4.5")
|
||||
# https://github.com/esphome/AsyncTCP/blob/master/library.json
|
||||
cg.add_library("esphome/AsyncTCP-esphome", "2.1.4")
|
||||
elif CORE.is_esp8266:
|
||||
# https://github.com/ESP32Async/ESPAsyncTCP
|
||||
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||
# https://github.com/esphome/ESPAsyncTCP
|
||||
cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0")
|
||||
|
||||
@@ -25,6 +25,7 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice
|
||||
|
||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
|
||||
void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
|
||||
void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "atm90e32.h"
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -849,7 +848,7 @@ uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t
|
||||
float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f;
|
||||
float target_voltage = nominal_voltage * multiplier;
|
||||
|
||||
float peak_01v = target_voltage * 100.0f * std::numbers::sqrt2_v<float>; // convert RMS → peak, scale to 0.01V
|
||||
float peak_01v = target_voltage * 100.0f * std::sqrt(2.0f); // convert RMS → peak, scale to 0.01V
|
||||
float divider = (2.0f * ugain) / 32768.0f;
|
||||
|
||||
float threshold = peak_01v / divider;
|
||||
|
||||
@@ -312,7 +312,7 @@ FileDecoderState AudioDecoder::decode_mp3_() {
|
||||
if (err) {
|
||||
switch (err) {
|
||||
case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY:
|
||||
[[fallthrough]];
|
||||
// Intentional fallthrough
|
||||
case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER:
|
||||
return FileDecoderState::FAILED;
|
||||
break;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
|
||||
#include "esp_crt_bundle.h"
|
||||
@@ -17,13 +16,13 @@ namespace audio {
|
||||
static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
|
||||
|
||||
static const uint32_t CONNECTION_TIMEOUT_MS = 5000;
|
||||
static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6;
|
||||
|
||||
// The number of times the http read times out with no data before throwing an error
|
||||
static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100;
|
||||
|
||||
static const size_t HTTP_STREAM_BUFFER_SIZE = 2048;
|
||||
|
||||
static const uint8_t MAX_REDIRECTIONS = 5;
|
||||
|
||||
static const char *const TAG = "audio_reader";
|
||||
static const uint8_t MAX_REDIRECTION = 5;
|
||||
|
||||
// Some common HTTP status codes - borrowed from http_request component accessed 20241224
|
||||
enum HttpStatus {
|
||||
@@ -95,7 +94,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
|
||||
client_config.url = uri.c_str();
|
||||
client_config.cert_pem = nullptr;
|
||||
client_config.disable_auto_redirect = false;
|
||||
client_config.max_redirection_count = MAX_REDIRECTIONS;
|
||||
client_config.max_redirection_count = 10;
|
||||
client_config.event_handler = http_event_handler;
|
||||
client_config.user_data = this;
|
||||
client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE;
|
||||
@@ -117,29 +116,12 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
|
||||
esp_err_t err = esp_http_client_open(this->client_, 0);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to open URL");
|
||||
this->cleanup_connection_();
|
||||
return err;
|
||||
}
|
||||
|
||||
int64_t header_length = esp_http_client_fetch_headers(this->client_);
|
||||
uint8_t reattempt_count = 0;
|
||||
while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) {
|
||||
this->cleanup_connection_();
|
||||
if (header_length != -ESP_ERR_HTTP_EAGAIN) {
|
||||
// Serious error, no recovery
|
||||
return ESP_FAIL;
|
||||
} else {
|
||||
// Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available
|
||||
this->client_ = esp_http_client_init(&client_config);
|
||||
esp_http_client_open(this->client_, 0);
|
||||
header_length = esp_http_client_fetch_headers(this->client_);
|
||||
++reattempt_count;
|
||||
}
|
||||
}
|
||||
|
||||
if (header_length < 0) {
|
||||
ESP_LOGE(TAG, "Failed to fetch headers");
|
||||
this->cleanup_connection_();
|
||||
return ESP_FAIL;
|
||||
}
|
||||
@@ -153,7 +135,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
|
||||
|
||||
ssize_t redirect_count = 0;
|
||||
|
||||
while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) {
|
||||
while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) {
|
||||
err = esp_http_client_open(this->client_, 0);
|
||||
if (err != ESP_OK) {
|
||||
this->cleanup_connection_();
|
||||
@@ -285,29 +267,27 @@ AudioReaderState AudioReader::http_read_() {
|
||||
return AudioReaderState::FINISHED;
|
||||
}
|
||||
} else if (this->output_transfer_buffer_->free() > 0) {
|
||||
int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(),
|
||||
this->output_transfer_buffer_->free());
|
||||
size_t bytes_to_read = this->output_transfer_buffer_->free();
|
||||
int received_len =
|
||||
esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read);
|
||||
|
||||
if (received_len > 0) {
|
||||
this->output_transfer_buffer_->increase_buffer_length(received_len);
|
||||
this->last_data_read_ms_ = millis();
|
||||
return AudioReaderState::READING;
|
||||
} else if (received_len <= 0) {
|
||||
} else if (received_len < 0) {
|
||||
// HTTP read error
|
||||
if (received_len == -1) {
|
||||
// A true connection error occured, no chance at recovery
|
||||
this->cleanup_connection_();
|
||||
return AudioReaderState::FAILED;
|
||||
}
|
||||
this->cleanup_connection_();
|
||||
return AudioReaderState::FAILED;
|
||||
} else {
|
||||
if (bytes_to_read > 0) {
|
||||
// Read timed out
|
||||
if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) {
|
||||
this->cleanup_connection_();
|
||||
return AudioReaderState::FAILED;
|
||||
}
|
||||
|
||||
// Read timed out, manually verify if it has been too long since the last successful read
|
||||
if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) {
|
||||
ESP_LOGE(TAG, "Timed out");
|
||||
this->cleanup_connection_();
|
||||
return AudioReaderState::FAILED;
|
||||
delay(READ_WRITE_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
delay(READ_WRITE_TIMEOUT_MS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
|
||||
bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
|
||||
this->buffer_size_ = buffer_size;
|
||||
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
RAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
|
||||
this->buffer_ = allocator.allocate(this->buffer_size_);
|
||||
if (this->buffer_ == nullptr) {
|
||||
@@ -101,7 +101,7 @@ bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
|
||||
|
||||
void AudioTransferBuffer::deallocate_buffer_() {
|
||||
if (this->buffer_ != nullptr) {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
RAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
allocator.deallocate(this->buffer_, this->buffer_size_);
|
||||
this->buffer_ = nullptr;
|
||||
this->data_start_ = nullptr;
|
||||
|
||||
@@ -16,6 +16,7 @@ class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListene
|
||||
|
||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; }
|
||||
void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
|
||||
|
||||
@@ -480,11 +480,7 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
|
||||
|
||||
/* Internal */
|
||||
|
||||
void BedJetHub::loop() {
|
||||
// Parent BLEClientNode has a loop() method, but this component uses
|
||||
// polling via update() and BLE callbacks so loop isn't needed
|
||||
this->disable_loop();
|
||||
}
|
||||
void BedJetHub::loop() {}
|
||||
void BedJetHub::update() { this->dispatch_status_(); }
|
||||
|
||||
void BedJetHub::dump_config() {
|
||||
|
||||
@@ -83,11 +83,7 @@ void BedJetClimate::reset_state_() {
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
void BedJetClimate::loop() {
|
||||
// This component is controlled via the parent BedJetHub
|
||||
// Empty loop not needed, disable to save CPU cycles
|
||||
this->disable_loop();
|
||||
}
|
||||
void BedJetClimate::loop() {}
|
||||
|
||||
void BedJetClimate::control(const ClimateCall &call) {
|
||||
ESP_LOGD(TAG, "Received BedJetClimate::control");
|
||||
|
||||
@@ -7,13 +7,11 @@
|
||||
|
||||
extern "C" {
|
||||
#include "rtos_pub.h"
|
||||
// rtos_pub.h must be included before the rest of the includes
|
||||
|
||||
#include "spi.h"
|
||||
#include "arm_arch.h"
|
||||
#include "general_dma_pub.h"
|
||||
#include "gpio_pub.h"
|
||||
#include "icu_pub.h"
|
||||
#include "spi.h"
|
||||
#undef SPI_DAT
|
||||
#undef SPI_BASE
|
||||
};
|
||||
@@ -126,7 +124,7 @@ void BekenSPILEDStripLightOutput::setup() {
|
||||
size_t buffer_size = this->get_buffer_size_();
|
||||
size_t dma_buffer_size = (buffer_size * 8) + (2 * 64);
|
||||
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
this->buf_ = allocator.allocate(buffer_size);
|
||||
if (this->buf_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Cannot allocate LED buffer!");
|
||||
|
||||
@@ -50,7 +50,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
|
||||
// turn on (after one-shot sensor automatically powers down)
|
||||
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
|
||||
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Power on failed");
|
||||
ESP_LOGW(TAG, "Turning on BH1750 failed");
|
||||
f(NAN);
|
||||
return;
|
||||
}
|
||||
@@ -60,7 +60,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
|
||||
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111);
|
||||
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111);
|
||||
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Set measurement time failed");
|
||||
ESP_LOGW(TAG, "Setting measurement time for BH1750 failed");
|
||||
active_mtreg_ = 0;
|
||||
f(NAN);
|
||||
return;
|
||||
@@ -88,7 +88,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
|
||||
return;
|
||||
}
|
||||
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Start measurement failed");
|
||||
ESP_LOGW(TAG, "Starting measurement for BH1750 failed");
|
||||
f(NAN);
|
||||
return;
|
||||
}
|
||||
@@ -99,7 +99,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
|
||||
this->set_timeout("read", meas_time, [this, mode, mtreg, f]() {
|
||||
uint16_t raw_value;
|
||||
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Read data failed");
|
||||
ESP_LOGW(TAG, "Reading BH1750 data failed");
|
||||
f(NAN);
|
||||
return;
|
||||
}
|
||||
@@ -156,7 +156,7 @@ void BH1750Sensor::update() {
|
||||
this->publish_state(NAN);
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val);
|
||||
ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), val);
|
||||
this->status_clear_warning();
|
||||
this->publish_state(val);
|
||||
});
|
||||
|
||||
@@ -60,8 +60,8 @@ from esphome.const import (
|
||||
DEVICE_CLASS_WINDOW,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
from esphome.util import Registry
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -148,7 +148,6 @@ BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Conditi
|
||||
|
||||
# Filters
|
||||
Filter = binary_sensor_ns.class_("Filter")
|
||||
TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter, cg.Component)
|
||||
DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component)
|
||||
DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component)
|
||||
DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component)
|
||||
@@ -172,19 +171,6 @@ async def invert_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(filter_id)
|
||||
|
||||
|
||||
@register_filter(
|
||||
"timeout",
|
||||
TimeoutFilter,
|
||||
cv.templatable(cv.positive_time_period_milliseconds),
|
||||
)
|
||||
async def timeout_filter_to_code(config, filter_id):
|
||||
var = cg.new_Pvariable(filter_id)
|
||||
await cg.register_component(var, {})
|
||||
template_ = await cg.templatable(config, [], cg.uint32)
|
||||
cg.add(var.set_timeout_value(template_))
|
||||
return var
|
||||
|
||||
|
||||
@register_filter(
|
||||
"delayed_on_off",
|
||||
DelayedOnOffFilter,
|
||||
@@ -505,9 +491,6 @@ _BINARY_SENSOR_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
_BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor"))
|
||||
|
||||
|
||||
def binary_sensor_schema(
|
||||
class_: MockObjClass = cv.UNDEFINED,
|
||||
*,
|
||||
@@ -538,7 +521,7 @@ BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
|
||||
|
||||
|
||||
async def setup_binary_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "binary_sensor")
|
||||
await setup_entity(var, config)
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
|
||||
@@ -25,12 +25,6 @@ void Filter::input(bool value) {
|
||||
}
|
||||
}
|
||||
|
||||
void TimeoutFilter::input(bool value) {
|
||||
this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
|
||||
// we do not de-dup here otherwise changes from invalid to valid state will not be output
|
||||
this->output(value);
|
||||
}
|
||||
|
||||
optional<bool> DelayedOnOffFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
|
||||
|
||||
@@ -16,7 +16,7 @@ class Filter {
|
||||
public:
|
||||
virtual optional<bool> new_value(bool value) = 0;
|
||||
|
||||
virtual void input(bool value);
|
||||
void input(bool value);
|
||||
|
||||
void output(bool value);
|
||||
|
||||
@@ -28,16 +28,6 @@ class Filter {
|
||||
Deduplicator<bool> dedup_;
|
||||
};
|
||||
|
||||
class TimeoutFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value) override { return value; }
|
||||
void input(bool value) override;
|
||||
template<typename T> void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; }
|
||||
|
||||
protected:
|
||||
TemplatableValue<uint32_t> timeout_delay_{};
|
||||
};
|
||||
|
||||
class DelayedOnOffFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
@@ -16,6 +16,7 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi
|
||||
public:
|
||||
void dump_config() override;
|
||||
void loop() override {}
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
|
||||
@@ -11,11 +11,7 @@ namespace ble_client {
|
||||
|
||||
static const char *const TAG = "ble_rssi_sensor";
|
||||
|
||||
void BLEClientRSSISensor::loop() {
|
||||
// Parent BLEClientNode has a loop() method, but this component uses
|
||||
// polling via update() and BLE GAP callbacks so loop isn't needed
|
||||
this->disable_loop();
|
||||
}
|
||||
void BLEClientRSSISensor::loop() {}
|
||||
|
||||
void BLEClientRSSISensor::dump_config() {
|
||||
LOG_SENSOR("", "BLE Client RSSI Sensor", this);
|
||||
|
||||
@@ -18,6 +18,7 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||
|
||||
|
||||
@@ -11,11 +11,7 @@ namespace ble_client {
|
||||
|
||||
static const char *const TAG = "ble_sensor";
|
||||
|
||||
void BLESensor::loop() {
|
||||
// Parent BLEClientNode has a loop() method, but this component uses
|
||||
// polling via update() and BLE callbacks so loop isn't needed
|
||||
this->disable_loop();
|
||||
}
|
||||
void BLESensor::loop() {}
|
||||
|
||||
void BLESensor::dump_config() {
|
||||
LOG_SENSOR("", "BLE Sensor", this);
|
||||
|
||||
@@ -24,6 +24,7 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
|
||||
@@ -19,6 +19,7 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie
|
||||
void loop() override {}
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user