Merge remote-tracking branch 'upstream/dev' into memory

This commit is contained in:
J. Nick Koston 2025-07-18 12:37:07 -10:00
commit 5503d9d7ee
No known key found for this signature in database
266 changed files with 9385 additions and 4121 deletions

222
.ai/instructions.md Normal file
View File

@ -0,0 +1,222 @@
# ESPHome AI Collaboration Guide
This document provides essential context for AI models interacting with this project. Adhering to these guidelines will ensure consistency and maintain code quality.
## 1. Project Overview & Purpose
* **Primary Goal:** ESPHome is a system to configure microcontrollers (like ESP32, ESP8266, RP2040, and LibreTiny-based chips) using simple yet powerful YAML configuration files. It generates C++ firmware that can be compiled and flashed to these devices, allowing users to control them remotely through home automation systems.
* **Business Domain:** Internet of Things (IoT), Home Automation.
## 2. Core Technologies & Stack
* **Languages:** Python (>=3.10), C++ (gnu++20)
* **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF.
* **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative.
* **Configuration:** YAML.
* **Key Libraries/Dependencies:**
* **Python:** `voluptuous` (for configuration validation), `PyYAML` (for parsing configuration files), `paho-mqtt` (for MQTT communication), `tornado` (for the web server), `aioesphomeapi` (for the native API).
* **C++:** `ArduinoJson` (for JSON serialization/deserialization), `AsyncMqttClient-esphome` (for MQTT), `ESPAsyncWebServer` (for the web server).
* **Package Manager(s):** `pip` (for Python dependencies), `platformio` (for C++/PlatformIO dependencies).
* **Communication Protocols:** Protobuf (for native API), MQTT, HTTP.
## 3. Architectural Patterns
* **Overall Architecture:** The project follows a code-generation architecture. The Python code parses user-defined YAML configuration files and generates C++ source code. This C++ code is then compiled and flashed to the target microcontroller using PlatformIO.
* **Directory Structure Philosophy:**
* `/esphome`: Contains the core Python source code for the ESPHome application.
* `/esphome/components`: Contains the individual components that can be used in ESPHome configurations. Each component is a self-contained unit with its own C++ and Python code.
* `/tests`: Contains all unit and integration tests for the Python code.
* `/docker`: Contains Docker-related files for building and running ESPHome in a container.
* `/script`: Contains helper scripts for development and maintenance.
* **Core Architectural Components:**
1. **Configuration System** (`esphome/config*.py`): Handles YAML parsing and validation using Voluptuous, schema definitions, and multi-platform configurations.
2. **Code Generation** (`esphome/codegen.py`, `esphome/cpp_generator.py`): Manages Python to C++ code generation, template processing, and build flag management.
3. **Component System** (`esphome/components/`): Contains modular hardware and software components with platform-specific implementations and dependency management.
4. **Core Framework** (`esphome/core/`): Manages the application lifecycle, hardware abstraction, and component registration.
5. **Dashboard** (`esphome/dashboard/`): A web-based interface for device configuration, management, and OTA updates.
* **Platform Support:**
1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (S2, S3, C3, etc.) and both IDF and Arduino frameworks.
2. **ESP8266** (`components/esp8266/`): Espressif ESP8266. Arduino framework only, with memory constraints.
3. **RP2040** (`components/rp2040/`): Raspberry Pi Pico/RP2040. Arduino framework with PIO (Programmable I/O) support.
4. **LibreTiny** (`components/libretiny/`): Realtek and Beken chips. Supports multiple chip families and auto-generated components.
## 4. Coding Conventions & Style Guide
* **Formatting:**
* **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`.
* **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`.
* **Naming Conventions:**
* **Python:** Follows PEP 8. Use clear, descriptive names following snake_case.
* **C++:** Follows the Google C++ Style Guide.
* **Component Structure:**
* **Standard Files:**
```
components/[component_name]/
├── __init__.py # Component configuration schema and code generation
├── [component].h # C++ header file (if needed)
├── [component].cpp # C++ implementation (if needed)
└── [platform]/ # Platform-specific implementations
├── __init__.py # Platform-specific configuration
├── [platform].h # Platform C++ header
└── [platform].cpp # Platform C++ implementation
```
* **Component Metadata:**
- `DEPENDENCIES`: List of required components
- `AUTO_LOAD`: Components to automatically load
- `CONFLICTS_WITH`: Incompatible components
- `CODEOWNERS`: GitHub usernames responsible for maintenance
- `MULTI_CONF`: Whether multiple instances are allowed
* **Code Generation & Common Patterns:**
* **Configuration Schema Pattern:**
```python
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_KEY, CONF_ID
CONF_PARAM = "param" # A constant that does not yet exist in esphome/const.py
my_component_ns = cg.esphome_ns.namespace("my_component")
MyComponent = my_component_ns.class_("MyComponent", cg.Component)
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(MyComponent),
cv.Required(CONF_KEY): cv.string,
cv.Optional(CONF_PARAM, default=42): cv.int_,
}).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_key(config[CONF_KEY]))
cg.add(var.set_param(config[CONF_PARAM]))
```
* **C++ Class Pattern:**
```cpp
namespace esphome {
namespace my_component {
class MyComponent : public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
void set_key(const std::string &key) { this->key_ = key; }
void set_param(int param) { this->param_ = param; }
protected:
std::string key_;
int param_{0};
};
} // namespace my_component
} // namespace esphome
```
* **Common Component Examples:**
- **Sensor:**
```python
from esphome.components import sensor
CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend(cv.polling_component_schema("60s"))
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
```
- **Binary Sensor:**
```python
from esphome.components import binary_sensor
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({ ... })
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
```
- **Switch:**
```python
from esphome.components import switch
CONFIG_SCHEMA = switch.switch_schema().extend({ ... })
async def to_code(config):
var = await switch.new_switch(config)
```
* **Configuration Validation:**
* **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`.
* **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
* **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `cv.only_with_arduino`.
* **Schema Extensions:**
```python
CONFIG_SCHEMA = cv.Schema({ ... })
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(i2c.i2c_device_schema(0x48))
.extend(spi.spi_device_schema(cs_pin_required=True))
```
## 5. Key Files & Entrypoints
* **Main Entrypoint(s):** `esphome/__main__.py` is the main entrypoint for the ESPHome command-line interface.
* **Configuration:**
* `pyproject.toml`: Defines the Python project metadata and dependencies.
* `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers.
* `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting.
* **CI/CD Pipeline:** Defined in `.github/workflows`.
## 6. Development & Testing Workflow
* **Local Development Environment:** Use the provided Docker container or create a Python virtual environment and install dependencies from `requirements_dev.txt`.
* **Running Commands:** Use the `script/run-in-env.py` script to execute commands within the project's virtual environment. For example, to run the linter: `python3 script/run-in-env.py pre-commit run`.
* **Testing:**
* **Python:** Run unit tests with `pytest`.
* **C++:** Use `clang-tidy` for static analysis.
* **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
```
tests/
├── test_build_components/ # Base test configurations
└── components/[component]/ # Component-specific tests
```
Run them using `script/test_build_components`. Use `-c <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.

View File

@ -1 +1 @@
07f621354fe1350ba51953c80273cd44a04aa44f15cc30bd7b8fe2a641427b7a 0c2acbc16bfb7d63571dbe7042f94f683be25e4ca8a0f158a960a94adac4b931

View File

@ -26,6 +26,7 @@
- [ ] RP2040 - [ ] RP2040
- [ ] BK72xx - [ ] BK72xx
- [ ] RTL87xx - [ ] RTL87xx
- [ ] nRF52840
## Example entry for `config.yaml`: ## Example entry for `config.yaml`:

1
.github/copilot-instructions.md vendored Symbolic link
View File

@ -0,0 +1 @@
../.ai/instructions.md

View File

@ -9,6 +9,9 @@ updates:
# Hypotehsis is only used for testing and is updated quite often # Hypotehsis is only used for testing and is updated quite often
- dependency-name: hypothesis - dependency-name: hypothesis
- package-ecosystem: github-actions - package-ecosystem: github-actions
labels:
- "dependencies"
- "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: daily interval: daily
@ -20,11 +23,17 @@ updates:
- "docker/login-action" - "docker/login-action"
- "docker/setup-buildx-action" - "docker/setup-buildx-action"
- package-ecosystem: github-actions - package-ecosystem: github-actions
labels:
- "dependencies"
- "github-actions"
directory: "/.github/actions/build-image" directory: "/.github/actions/build-image"
schedule: schedule:
interval: daily interval: daily
open-pull-requests-limit: 10 open-pull-requests-limit: 10
- package-ecosystem: github-actions - package-ecosystem: github-actions
labels:
- "dependencies"
- "github-actions"
directory: "/.github/actions/restore-python" directory: "/.github/actions/restore-python"
schedule: schedule:
interval: daily interval: daily

449
.github/workflows/auto-label-pr.yml vendored Normal file
View File

@ -0,0 +1,449 @@
name: Auto Label PR
on:
# Runs only on pull_request_target due to having access to a App token.
# This means PRs from forks will not be able to alter this workflow to get the tokens
pull_request_target:
types: [labeled, opened, reopened, synchronize, edited]
permissions:
pull-requests: write
contents: read
env:
TARGET_PLATFORMS: |
esp32
esp8266
rp2040
libretiny
bk72xx
rtl87xx
ln882x
nrf52
host
PLATFORM_COMPONENTS: |
alarm_control_panel
audio_adc
audio_dac
binary_sensor
button
canbus
climate
cover
datetime
display
event
fan
light
lock
media_player
microphone
number
one_wire
ota
output
packet_transport
select
sensor
speaker
stepper
switch
text
text_sensor
time
touchscreen
update
valve
SMALL_PR_THRESHOLD: 30
MAX_LABELS: 15
jobs:
label:
runs-on: ubuntu-latest
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Get changes
id: changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get PR number
pr_number="${{ github.event.pull_request.number }}"
# Get list of changed files using gh CLI
files=$(gh pr diff $pr_number --name-only)
echo "files<<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);
}
}

View File

@ -47,7 +47,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.10" python-version: "3.11"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1 uses: docker/setup-buildx-action@v3.11.1

View File

@ -20,8 +20,8 @@ permissions:
contents: read contents: read
env: env:
DEFAULT_PYTHON: "3.10" DEFAULT_PYTHON: "3.11"
PYUPGRADE_TARGET: "--py310-plus" PYUPGRADE_TARGET: "--py311-plus"
concurrency: concurrency:
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@ -112,7 +112,6 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: python-version:
- "3.10"
- "3.11" - "3.11"
- "3.12" - "3.12"
- "3.13" - "3.13"
@ -128,14 +127,10 @@ jobs:
os: windows-latest os: windows-latest
- python-version: "3.12" - python-version: "3.12"
os: windows-latest os: windows-latest
- python-version: "3.10"
os: windows-latest
- python-version: "3.13" - python-version: "3.13"
os: macOS-latest os: macOS-latest
- python-version: "3.12" - python-version: "3.12"
os: macOS-latest os: macOS-latest
- python-version: "3.10"
os: macOS-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: needs:
- common - common

View File

@ -0,0 +1,264 @@
# This workflow automatically requests reviews from codeowners when:
# 1. A PR is opened, reopened, or synchronized (updated)
# 2. A PR is marked as ready for review
#
# It reads the CODEOWNERS file and matches all changed files in the PR against
# the codeowner patterns, then requests reviews from the appropriate owners
# while avoiding duplicate requests for users who have already been requested
# or have already reviewed the PR.
name: Request Codeowner Reviews
on:
# Needs to be pull_request_target to get write permissions
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
permissions:
pull-requests: write
contents: read
jobs:
request-codeowner-reviews:
name: Run
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
steps:
- name: Request reviews from component codeowners
uses: actions/github-script@v7.0.1
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const pr_number = context.payload.pull_request.number;
console.log(`Processing PR #${pr_number} for codeowner review requests`);
try {
// Get the list of changed files in this PR
const { data: files } = await github.rest.pulls.listFiles({
owner,
repo,
pull_number: pr_number
});
const changedFiles = files.map(file => file.filename);
console.log(`Found ${changedFiles.length} changed files`);
if (changedFiles.length === 0) {
console.log('No changed files found, skipping codeowner review requests');
return;
}
// Fetch CODEOWNERS file from root
const { data: codeownersFile } = await github.rest.repos.getContent({
owner,
repo,
path: 'CODEOWNERS',
ref: context.payload.pull_request.base.sha
});
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
// Parse CODEOWNERS file to extract all patterns and their owners
const codeownersLines = codeownersContent.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
const codeownersPatterns = [];
// Convert CODEOWNERS pattern to regex (robust glob handling)
function globToRegex(pattern) {
// Escape regex special characters except for glob wildcards
let regexStr = pattern
.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars
.replace(/\*\*/g, '.*') // globstar
.replace(/\*/g, '[^/]*') // single star
.replace(/\?/g, '.'); // question mark
return new RegExp('^' + regexStr + '$');
}
// Helper function to create comment body
function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) {
const reviewerMentions = reviewersList.map(r => `@${r}`);
const teamMentions = teamsList.map(t => `@${owner}/${t}`);
const allMentions = [...reviewerMentions, ...teamMentions].join(', ');
if (isSuccessful) {
return `👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! 🙏`;
} else {
return `👋 Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! 🙏\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`;
}
}
for (const line of codeownersLines) {
const parts = line.split(/\s+/);
if (parts.length < 2) continue;
const pattern = parts[0];
const owners = parts.slice(1);
// Use robust glob-to-regex conversion
const regex = globToRegex(pattern);
codeownersPatterns.push({ pattern, regex, owners });
}
console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
// Match changed files against CODEOWNERS patterns
const matchedOwners = new Set();
const matchedTeams = new Set();
const fileMatches = new Map(); // Track which files matched which patterns
for (const file of changedFiles) {
for (const { pattern, regex, owners } of codeownersPatterns) {
if (regex.test(file)) {
console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`);
if (!fileMatches.has(file)) {
fileMatches.set(file, []);
}
fileMatches.get(file).push({ pattern, owners });
// Add owners to the appropriate set (remove @ prefix)
for (const owner of owners) {
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
if (cleanOwner.includes('/')) {
// Team mention (org/team-name)
const teamName = cleanOwner.split('/')[1];
matchedTeams.add(teamName);
} else {
// Individual user
matchedOwners.add(cleanOwner);
}
}
}
}
}
if (matchedOwners.size === 0 && matchedTeams.size === 0) {
console.log('No codeowners found for any changed files');
return;
}
// Remove the PR author from reviewers
const prAuthor = context.payload.pull_request.user.login;
matchedOwners.delete(prAuthor);
// Get current reviewers to avoid duplicate requests (but still mention them)
const { data: prData } = await github.rest.pulls.get({
owner,
repo,
pull_number: pr_number
});
const currentReviewers = new Set();
const currentTeams = new Set();
if (prData.requested_reviewers) {
prData.requested_reviewers.forEach(reviewer => {
currentReviewers.add(reviewer.login);
});
}
if (prData.requested_teams) {
prData.requested_teams.forEach(team => {
currentTeams.add(team.slug);
});
}
// Check for completed reviews to avoid re-requesting users who have already reviewed
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: pr_number
});
const reviewedUsers = new Set();
reviews.forEach(review => {
reviewedUsers.add(review.user.login);
});
// Remove only users who have already submitted reviews (not just requested reviewers)
reviewedUsers.forEach(reviewer => {
matchedOwners.delete(reviewer);
});
// For teams, we'll still remove already requested teams to avoid API errors
currentTeams.forEach(team => {
matchedTeams.delete(team);
});
const reviewersList = Array.from(matchedOwners);
const teamsList = Array.from(matchedTeams);
if (reviewersList.length === 0 && teamsList.length === 0) {
console.log('No eligible reviewers found (all may already be requested or reviewed)');
return;
}
const totalReviewers = reviewersList.length + teamsList.length;
console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`);
// Request reviews
try {
const requestParams = {
owner,
repo,
pull_number: pr_number
};
// Filter out users who are already requested reviewers for the API call
const newReviewers = reviewersList.filter(reviewer => !currentReviewers.has(reviewer));
const newTeams = teamsList.filter(team => !currentTeams.has(team));
if (newReviewers.length > 0) {
requestParams.reviewers = newReviewers;
}
if (newTeams.length > 0) {
requestParams.team_reviewers = newTeams;
}
// Only make the API call if there are new reviewers to request
if (newReviewers.length > 0 || newTeams.length > 0) {
await github.rest.pulls.requestReviewers(requestParams);
console.log(`Successfully requested reviews from ${newReviewers.length} new users and ${newTeams.length} new teams`);
} else {
console.log('All codeowners are already requested reviewers or have reviewed');
}
// Add a comment to the PR mentioning what happened (include all matched codeowners)
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: commentBody
});
} catch (error) {
if (error.status === 422) {
console.log('Some reviewers may already be requested or unavailable:', error.message);
// Try to add a comment even if review request failed
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
try {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: commentBody
});
} catch (commentError) {
console.log('Failed to add comment:', commentError.message);
}
} else {
throw error;
}
}
} catch (error) {
console.log('Failed to process codeowner review requests:', error.message);
console.error(error);
}

View File

@ -0,0 +1,147 @@
name: Add External Component Comment
on:
pull_request_target:
types: [opened, synchronize]
permissions:
contents: read # Needed to fetch PR details
issues: write # Needed to create and update comments (PR comments are managed via the issues REST API)
pull-requests: write # also needed?
jobs:
external-comment:
name: External component comment
runs-on: ubuntu-latest
steps:
- name: Add external component comment
uses: actions/github-script@v7.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Generate external component usage instructions
function generateExternalComponentInstructions(prNumber, componentNames, owner, repo) {
let source;
if (owner === 'esphome' && repo === 'esphome')
source = `github://pr#${prNumber}`;
else
source = `github://${owner}/${repo}@pull/${prNumber}/head`;
return `To use the changes from this PR as an external component, add the following to your ESPHome configuration YAML file:
\`\`\`yaml
external_components:
- source: ${source}
components: [${componentNames.join(', ')}]
refresh: 1h
\`\`\``;
}
// Generate repo clone instructions
function generateRepoInstructions(prNumber, owner, repo, branch) {
return `To use the changes in this PR:
\`\`\`bash
# Clone the repository:
git clone https://github.com/${owner}/${repo}
cd ${repo}
# Checkout the PR branch:
git fetch origin pull/${prNumber}/head:${branch}
git checkout ${branch}
# Install the development version:
script/setup
# Activate the development version:
source venv/bin/activate
\`\`\`
Now you can run \`esphome\` as usual to test the changes in this PR.
`;
}
async function createComment(octokit, owner, repo, prNumber, esphomeChanges, componentChanges) {
const commentMarker = "<!-- 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);
}

View File

@ -0,0 +1,119 @@
# This workflow automatically notifies codeowners when an issue is labeled with component labels.
# It reads the CODEOWNERS file to find the maintainers for the labeled components
# and posts a comment mentioning them to ensure they're aware of the issue.
name: Notify Issue Codeowners
on:
issues:
types: [labeled]
permissions:
issues: write
contents: read
jobs:
notify-codeowners:
name: Run
if: ${{ startsWith(github.event.label.name, format('component{0} ', ':')) }}
runs-on: ubuntu-latest
steps:
- name: Notify codeowners for component issues
uses: actions/github-script@v7.0.1
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const issue_number = context.payload.issue.number;
const labelName = context.payload.label.name;
console.log(`Processing issue #${issue_number} with label: ${labelName}`);
// Extract component name from label
const componentName = labelName.replace('component: ', '');
console.log(`Component: ${componentName}`);
try {
// Fetch CODEOWNERS file from root
const { data: codeownersFile } = await github.rest.repos.getContent({
owner,
repo,
path: 'CODEOWNERS'
});
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
// Parse CODEOWNERS file to extract component mappings
const codeownersLines = codeownersContent.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
let componentOwners = null;
for (const line of codeownersLines) {
const parts = line.split(/\s+/);
if (parts.length < 2) continue;
const pattern = parts[0];
const owners = parts.slice(1);
// Look for component patterns: esphome/components/{component}/*
const componentMatch = pattern.match(/^esphome\/components\/([^\/]+)\/\*$/);
if (componentMatch && componentMatch[1] === componentName) {
componentOwners = owners;
break;
}
}
if (!componentOwners) {
console.log(`No codeowners found for component: ${componentName}`);
return;
}
console.log(`Found codeowners for '${componentName}': ${componentOwners.join(', ')}`);
// Separate users and teams
const userOwners = [];
const teamOwners = [];
for (const owner of componentOwners) {
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
if (cleanOwner.includes('/')) {
// Team mention (org/team-name)
teamOwners.push(`@${cleanOwner}`);
} else {
// Individual user
userOwners.push(`@${cleanOwner}`);
}
}
// Remove issue author from mentions to avoid self-notification
const issueAuthor = context.payload.issue.user.login;
const filteredUserOwners = userOwners.filter(mention =>
mention !== `@${issueAuthor}`
);
const allMentions = [...filteredUserOwners, ...teamOwners];
if (allMentions.length === 0) {
console.log('No codeowners to notify (issue author is the only codeowner)');
return;
}
// Create comment body
const mentionString = allMentions.join(', ');
const commentBody = `👋 Hey ${mentionString}!\n\nThis issue has been labeled with \`component: ${componentName}\` and you've been identified as a codeowner of this component. Please take a look when you have a chance!\n\nThanks for maintaining this component! 🙏`;
// Post comment
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue_number,
body: commentBody
});
console.log(`Successfully notified codeowners: ${mentionString}`);
} catch (error) {
console.log('Failed to process codeowner notifications:', error.message);
console.error(error);
}

View File

@ -96,7 +96,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.10" python-version: "3.11"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1 uses: docker/setup-buildx-action@v3.11.1

View File

@ -11,7 +11,7 @@ ci:
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.12.3 rev: v0.12.4
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@ -40,7 +40,7 @@ repos:
rev: v3.20.0 rev: v3.20.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py310-plus] args: [--py311-plus]
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1 rev: v1.37.1
hooks: hooks:

1
CLAUDE.md Symbolic link
View File

@ -0,0 +1 @@
.ai/instructions.md

View File

@ -324,6 +324,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw
esphome/components/nfc/* @jesserockz @kbx81 esphome/components/nfc/* @jesserockz @kbx81
esphome/components/noblex/* @AGalfra esphome/components/noblex/* @AGalfra
esphome/components/npi19/* @bakerkj esphome/components/npi19/* @bakerkj
esphome/components/nrf52/* @tomaszduda23
esphome/components/number/* @esphome/core esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @clydebarrow @guillempages esphome/components/online_image/* @clydebarrow @guillempages
@ -378,6 +379,7 @@ esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet
esphome/components/runtime_stats/* @bdraco
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core esphome/components/script/* @esphome/core
@ -535,5 +537,6 @@ esphome/components/xiaomi_xmwsdj04mmc/* @medusalix
esphome/components/xl9535/* @mreditor97 esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zio_ultrasonic/* @kahrendt esphome/components/zio_ultrasonic/* @kahrendt

View File

@ -7,7 +7,7 @@ project and be sure to join us on [Discord](https://discord.gg/KhAMKrd).
**See also:** **See also:**
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues) [Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/esphome/issues) -- [Feature requests](https://github.com/orgs/esphome/discussions)
--- ---

1
GEMINI.md Symbolic link
View File

@ -0,0 +1 @@
.ai/instructions.md

View File

@ -9,7 +9,7 @@
--- ---
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues) [Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/esphome/issues) -- [Feature requests](https://github.com/orgs/esphome/discussions)
--- ---

View File

@ -5,6 +5,7 @@ from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32S2, VARIANT_ESP32S2,
@ -51,82 +52,93 @@ SAMPLING_MODES = {
"max": sampling_mode.MAX, "max": sampling_mode.MAX,
} }
adc1_channel_t = cg.global_ns.enum("adc1_channel_t") adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True)
adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
adc_channel_t = cg.global_ns.enum("adc_channel_t", is_class=True)
# pin to adc1 channel mapping # pin to adc1 channel mapping
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h # https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
VARIANT_ESP32: { VARIANT_ESP32: {
36: adc1_channel_t.ADC1_CHANNEL_0, 36: adc_channel_t.ADC_CHANNEL_0,
37: adc1_channel_t.ADC1_CHANNEL_1, 37: adc_channel_t.ADC_CHANNEL_1,
38: adc1_channel_t.ADC1_CHANNEL_2, 38: adc_channel_t.ADC_CHANNEL_2,
39: adc1_channel_t.ADC1_CHANNEL_3, 39: adc_channel_t.ADC_CHANNEL_3,
32: adc1_channel_t.ADC1_CHANNEL_4, 32: adc_channel_t.ADC_CHANNEL_4,
33: adc1_channel_t.ADC1_CHANNEL_5, 33: adc_channel_t.ADC_CHANNEL_5,
34: adc1_channel_t.ADC1_CHANNEL_6, 34: adc_channel_t.ADC_CHANNEL_6,
35: adc1_channel_t.ADC1_CHANNEL_7, 35: adc_channel_t.ADC_CHANNEL_7,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
VARIANT_ESP32C2: { VARIANT_ESP32C2: {
0: adc1_channel_t.ADC1_CHANNEL_0, 0: adc_channel_t.ADC_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1, 1: adc_channel_t.ADC_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2, 2: adc_channel_t.ADC_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3, 3: adc_channel_t.ADC_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4, 4: adc_channel_t.ADC_CHANNEL_4,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
VARIANT_ESP32C3: { VARIANT_ESP32C3: {
0: adc1_channel_t.ADC1_CHANNEL_0, 0: adc_channel_t.ADC_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1, 1: adc_channel_t.ADC_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2, 2: adc_channel_t.ADC_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3, 3: adc_channel_t.ADC_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4, 4: adc_channel_t.ADC_CHANNEL_4,
},
# ESP32-C5 ADC1 pin mapping - based on official ESP-IDF documentation
# https://docs.espressif.com/projects/esp-idf/en/latest/esp32c5/api-reference/peripherals/gpio.html
VARIANT_ESP32C5: {
1: adc_channel_t.ADC_CHANNEL_0,
2: adc_channel_t.ADC_CHANNEL_1,
3: adc_channel_t.ADC_CHANNEL_2,
4: adc_channel_t.ADC_CHANNEL_3,
5: adc_channel_t.ADC_CHANNEL_4,
6: adc_channel_t.ADC_CHANNEL_5,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: { VARIANT_ESP32C6: {
0: adc1_channel_t.ADC1_CHANNEL_0, 0: adc_channel_t.ADC_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1, 1: adc_channel_t.ADC_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2, 2: adc_channel_t.ADC_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3, 3: adc_channel_t.ADC_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4, 4: adc_channel_t.ADC_CHANNEL_4,
5: adc1_channel_t.ADC1_CHANNEL_5, 5: adc_channel_t.ADC_CHANNEL_5,
6: adc1_channel_t.ADC1_CHANNEL_6, 6: adc_channel_t.ADC_CHANNEL_6,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: { VARIANT_ESP32H2: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc_channel_t.ADC_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2, 3: adc_channel_t.ADC_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3, 4: adc_channel_t.ADC_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4, 5: adc_channel_t.ADC_CHANNEL_4,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc_channel_t.ADC_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2, 3: adc_channel_t.ADC_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3, 4: adc_channel_t.ADC_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4, 5: adc_channel_t.ADC_CHANNEL_4,
6: adc1_channel_t.ADC1_CHANNEL_5, 6: adc_channel_t.ADC_CHANNEL_5,
7: adc1_channel_t.ADC1_CHANNEL_6, 7: adc_channel_t.ADC_CHANNEL_6,
8: adc1_channel_t.ADC1_CHANNEL_7, 8: adc_channel_t.ADC_CHANNEL_7,
9: adc1_channel_t.ADC1_CHANNEL_8, 9: adc_channel_t.ADC_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9, 10: adc_channel_t.ADC_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
VARIANT_ESP32S3: { VARIANT_ESP32S3: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc_channel_t.ADC_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2, 3: adc_channel_t.ADC_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3, 4: adc_channel_t.ADC_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4, 5: adc_channel_t.ADC_CHANNEL_4,
6: adc1_channel_t.ADC1_CHANNEL_5, 6: adc_channel_t.ADC_CHANNEL_5,
7: adc1_channel_t.ADC1_CHANNEL_6, 7: adc_channel_t.ADC_CHANNEL_6,
8: adc1_channel_t.ADC1_CHANNEL_7, 8: adc_channel_t.ADC_CHANNEL_7,
9: adc1_channel_t.ADC1_CHANNEL_8, 9: adc_channel_t.ADC_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9, 10: adc_channel_t.ADC_CHANNEL_9,
}, },
} }
@ -135,54 +147,56 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
ESP32_VARIANT_ADC2_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 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
VARIANT_ESP32: { VARIANT_ESP32: {
4: adc2_channel_t.ADC2_CHANNEL_0, 4: adc_channel_t.ADC_CHANNEL_0,
0: adc2_channel_t.ADC2_CHANNEL_1, 0: adc_channel_t.ADC_CHANNEL_1,
2: adc2_channel_t.ADC2_CHANNEL_2, 2: adc_channel_t.ADC_CHANNEL_2,
15: adc2_channel_t.ADC2_CHANNEL_3, 15: adc_channel_t.ADC_CHANNEL_3,
13: adc2_channel_t.ADC2_CHANNEL_4, 13: adc_channel_t.ADC_CHANNEL_4,
12: adc2_channel_t.ADC2_CHANNEL_5, 12: adc_channel_t.ADC_CHANNEL_5,
14: adc2_channel_t.ADC2_CHANNEL_6, 14: adc_channel_t.ADC_CHANNEL_6,
27: adc2_channel_t.ADC2_CHANNEL_7, 27: adc_channel_t.ADC_CHANNEL_7,
25: adc2_channel_t.ADC2_CHANNEL_8, 25: adc_channel_t.ADC_CHANNEL_8,
26: adc2_channel_t.ADC2_CHANNEL_9, 26: adc_channel_t.ADC_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
VARIANT_ESP32C2: { VARIANT_ESP32C2: {
5: adc2_channel_t.ADC2_CHANNEL_0, 5: adc_channel_t.ADC_CHANNEL_0,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
VARIANT_ESP32C3: { VARIANT_ESP32C3: {
5: adc2_channel_t.ADC2_CHANNEL_0, 5: adc_channel_t.ADC_CHANNEL_0,
}, },
# ESP32-C5 has no ADC2 channels
VARIANT_ESP32C5: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {}, # no ADC2 VARIANT_ESP32C6: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {}, # no ADC2 VARIANT_ESP32H2: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
11: adc2_channel_t.ADC2_CHANNEL_0, 11: adc_channel_t.ADC_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1, 12: adc_channel_t.ADC_CHANNEL_1,
13: adc2_channel_t.ADC2_CHANNEL_2, 13: adc_channel_t.ADC_CHANNEL_2,
14: adc2_channel_t.ADC2_CHANNEL_3, 14: adc_channel_t.ADC_CHANNEL_3,
15: adc2_channel_t.ADC2_CHANNEL_4, 15: adc_channel_t.ADC_CHANNEL_4,
16: adc2_channel_t.ADC2_CHANNEL_5, 16: adc_channel_t.ADC_CHANNEL_5,
17: adc2_channel_t.ADC2_CHANNEL_6, 17: adc_channel_t.ADC_CHANNEL_6,
18: adc2_channel_t.ADC2_CHANNEL_7, 18: adc_channel_t.ADC_CHANNEL_7,
19: adc2_channel_t.ADC2_CHANNEL_8, 19: adc_channel_t.ADC_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9, 20: adc_channel_t.ADC_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
VARIANT_ESP32S3: { VARIANT_ESP32S3: {
11: adc2_channel_t.ADC2_CHANNEL_0, 11: adc_channel_t.ADC_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1, 12: adc_channel_t.ADC_CHANNEL_1,
13: adc2_channel_t.ADC2_CHANNEL_2, 13: adc_channel_t.ADC_CHANNEL_2,
14: adc2_channel_t.ADC2_CHANNEL_3, 14: adc_channel_t.ADC_CHANNEL_3,
15: adc2_channel_t.ADC2_CHANNEL_4, 15: adc_channel_t.ADC_CHANNEL_4,
16: adc2_channel_t.ADC2_CHANNEL_5, 16: adc_channel_t.ADC_CHANNEL_5,
17: adc2_channel_t.ADC2_CHANNEL_6, 17: adc_channel_t.ADC_CHANNEL_6,
18: adc2_channel_t.ADC2_CHANNEL_7, 18: adc_channel_t.ADC_CHANNEL_7,
19: adc2_channel_t.ADC2_CHANNEL_8, 19: adc_channel_t.ADC_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9, 20: adc_channel_t.ADC_CHANNEL_9,
}, },
} }

View File

@ -3,12 +3,15 @@
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h" #include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_adc_cal.h> #include "esp_adc/adc_cali.h"
#include "driver/adc.h" #include "esp_adc/adc_cali_scheme.h"
#endif // USE_ESP32 #include "esp_adc/adc_oneshot.h"
#include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX
#endif // USE_ESP32
namespace esphome { namespace esphome {
namespace adc { namespace adc {
@ -49,36 +52,72 @@ class Aggregator {
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
public: public:
#ifdef USE_ESP32 /// Update the sensor's state by reading the current ADC value.
/// Set the attenuation for this pin. Only available on the ESP32. /// This method is called periodically based on the update interval.
void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; }
void set_channel1(adc1_channel_t channel) {
this->channel1_ = channel;
this->channel2_ = ADC2_CHANNEL_MAX;
}
void set_channel2(adc2_channel_t channel) {
this->channel2_ = channel;
this->channel1_ = ADC1_CHANNEL_MAX;
}
void set_autorange(bool autorange) { this->autorange_ = autorange; }
#endif // USE_ESP32
/// Update ADC values
void update() override; void update() override;
/// Setup ADC
/// Set up the ADC sensor by initializing hardware and calibration parameters.
/// This method is called once during device initialization.
void setup() override; 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; void dump_config() override;
/// `HARDWARE_LATE` setup priority
/// Return the setup priority for this component.
/// Components with higher priority are initialized earlier during setup.
/// @return A float representing the setup priority.
float get_setup_priority() const override; 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; } 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; } 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); 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); 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; float sample() override;
#ifdef USE_ESP8266 #ifdef USE_ESP32
std::string unique_id() override; /// Set the ADC attenuation level to adjust the input voltage range.
#endif // USE_ESP8266 /// This determines how the ADC interprets input voltages, allowing for greater precision
/// or the ability to measure higher voltages depending on the chosen attenuation level.
/// @param attenuation The desired ADC attenuation level (e.g., ADC_ATTEN_DB_0, ADC_ATTEN_DB_11).
void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; }
/// Configure the ADC to use a specific channel on a specific ADC unit.
/// This sets the channel for single-shot or continuous ADC measurements.
/// @param unit The ADC unit to use (ADC_UNIT_1 or ADC_UNIT_2).
/// @param channel The ADC channel to configure, such as ADC_CHANNEL_0, ADC_CHANNEL_3, etc.
void set_channel(adc_unit_t unit, adc_channel_t channel) {
this->adc_unit_ = unit;
this->channel_ = channel;
}
/// Set whether autoranging should be enabled for the ADC.
/// Autoranging automatically adjusts the attenuation level to handle a wide range of input voltages.
/// @param autorange Boolean indicating whether to enable autoranging.
void set_autorange(bool autorange) { this->autorange_ = autorange; }
#endif // USE_ESP32
#ifdef USE_RP2040 #ifdef USE_RP2040
void set_is_temperature() { this->is_temperature_ = true; } void set_is_temperature() { this->is_temperature_ = true; }
@ -90,17 +129,28 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
InternalGPIOPin *pin_; InternalGPIOPin *pin_;
SamplingMode sampling_mode_{SamplingMode::AVG}; 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 #ifdef USE_RP2040
bool is_temperature_{false}; bool is_temperature_{false};
#endif // USE_RP2040 #endif // USE_RP2040
#ifdef USE_ESP32
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
adc1_channel_t channel1_{ADC1_CHANNEL_MAX};
adc2_channel_t channel2_{ADC2_CHANNEL_MAX};
bool autorange_{false};
esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {};
#endif // USE_ESP32
}; };
} // namespace adc } // namespace adc

View File

@ -8,145 +8,315 @@ namespace adc {
static const char *const TAG = "adc.esp32"; static const char *const TAG = "adc.esp32";
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1); adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr};
#ifndef SOC_ADC_RTC_MAX_BITWIDTH const LogString *attenuation_to_str(adc_atten_t attenuation) {
#if USE_ESP32_VARIANT_ESP32S2 switch (attenuation) {
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; case ADC_ATTEN_DB_0:
#else return LOG_STR("0 dB");
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; case ADC_ATTEN_DB_2_5:
#endif // USE_ESP32_VARIANT_ESP32S2 return LOG_STR("2.5 dB");
#endif // SOC_ADC_RTC_MAX_BITWIDTH case ADC_ATTEN_DB_6:
return LOG_STR("6 dB");
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; case ADC_ATTEN_DB_12_COMPAT:
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; return LOG_STR("12 dB");
default:
void ADCSensor::setup() { return LOG_STR("Unknown Attenuation");
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
if (this->channel1_ != ADC1_CHANNEL_MAX) {
adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
if (!this->autorange_) {
adc1_config_channel_atten(this->channel1_, this->attenuation_);
}
} else if (this->channel2_ != ADC2_CHANNEL_MAX) {
if (!this->autorange_) {
adc2_config_channel_atten(this->channel2_, this->attenuation_);
}
}
for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) {
auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2;
auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS,
1100, // default vref
&this->cal_characteristics_[i]);
switch (cal_value) {
case ESP_ADC_CAL_VAL_EFUSE_VREF:
ESP_LOGV(TAG, "Using eFuse Vref for calibration");
break;
case ESP_ADC_CAL_VAL_EFUSE_TP:
ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration");
break;
case ESP_ADC_CAL_VAL_DEFAULT_VREF:
default:
break;
}
} }
} }
void ADCSensor::dump_config() { const LogString *adc_unit_to_str(adc_unit_t unit) {
static const char *const ATTEN_AUTO_STR = "auto"; switch (unit) {
static const char *const ATTEN_0DB_STR = "0 db"; case ADC_UNIT_1:
static const char *const ATTEN_2_5DB_STR = "2.5 db"; return LOG_STR("ADC1");
static const char *const ATTEN_6DB_STR = "6 db"; case ADC_UNIT_2:
static const char *const ATTEN_12DB_STR = "12 db"; return LOG_STR("ADC2");
const char *atten_str = ATTEN_AUTO_STR; default:
return LOG_STR("Unknown ADC Unit");
}
}
LOG_SENSOR("", "ADC Sensor", this); void ADCSensor::setup() {
LOG_PIN(" Pin: ", this->pin_); ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
// Check if another sensor already initialized this ADC unit
if (!this->autorange_) { if (ADCSensor::shared_adc_handles[this->adc_unit_] == nullptr) {
switch (this->attenuation_) { adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
case ADC_ATTEN_DB_0: init_config.unit_id = this->adc_unit_;
atten_str = ATTEN_0DB_STR; init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
break; #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2
case ADC_ATTEN_DB_2_5: init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT;
atten_str = ATTEN_2_5DB_STR; #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 ||
break; // USE_ESP32_VARIANT_ESP32H2
case ADC_ATTEN_DB_6: esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]);
atten_str = ATTEN_6DB_STR; if (err != ESP_OK) {
break; ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err);
case ADC_ATTEN_DB_12_COMPAT: this->mark_failed();
atten_str = ATTEN_12DB_STR; return;
break;
default: // This is to satisfy the unused ADC_ATTEN_MAX
break;
} }
} }
this->adc_handle_ = ADCSensor::shared_adc_handles[this->adc_unit_];
this->setup_flags_.handle_init_complete = true;
adc_oneshot_chan_cfg_t config = {
.atten = this->attenuation_,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error configuring channel: %d", err);
this->mark_failed();
return;
}
this->setup_flags_.config_complete = true;
// Initialize ADC calibration
if (this->calibration_handle_ == nullptr) {
adc_cali_handle_t handle = nullptr;
esp_err_t err;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
// RISC-V variants and S3 use curve fitting calibration
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.unit_id = this->adc_unit_;
cali_config.atten = this->attenuation_;
cali_config.bitwidth = ADC_BITWIDTH_DEFAULT;
err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
if (err == ESP_OK) {
this->calibration_handle_ = handle;
this->setup_flags_.calibration_complete = true;
ESP_LOGV(TAG, "Using curve fitting calibration");
} else {
ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#else // Other ESP32 variants use line fitting calibration
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = this->attenuation_,
.bitwidth = ADC_BITWIDTH_DEFAULT,
#if !defined(USE_ESP32_VARIANT_ESP32S2)
.default_vref = 1100, // Default reference voltage in mV
#endif // !defined(USE_ESP32_VARIANT_ESP32S2)
};
err = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
if (err == ESP_OK) {
this->calibration_handle_ = handle;
this->setup_flags_.calibration_complete = true;
ESP_LOGV(TAG, "Using line fitting calibration");
} else {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
}
this->setup_flags_.init_complete = true;
}
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" Attenuation: %s\n" " Channel: %d\n"
" Samples: %i\n" " Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s", " Sampling mode: %s",
atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
ESP_LOGCONFIG(
TAG,
" Setup Status:\n"
" Handle Init: %s\n"
" Config: %s\n"
" Calibration: %s\n"
" Overall Init: %s",
this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED",
this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED");
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
} }
float ADCSensor::sample() { float ADCSensor::sample() {
if (!this->autorange_) { if (this->autorange_) {
auto aggr = Aggregator(this->sampling_mode_); return this->sample_autorange_();
} else {
return this->sample_fixed_attenuation_();
}
}
for (uint8_t sample = 0; sample < this->sample_count_; sample++) { float ADCSensor::sample_fixed_attenuation_() {
int raw = -1; auto aggr = Aggregator(this->sampling_mode_);
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;
}
aggr.add_sample(raw); for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
int raw;
esp_err_t err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw);
if (err != ESP_OK) {
ESP_LOGW(TAG, "ADC read failed with error %d", err);
continue;
} }
if (this->output_raw_) {
return aggr.aggregate(); if (raw == -1) {
ESP_LOGW(TAG, "Invalid ADC reading");
continue;
} }
uint32_t mv =
esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]); aggr.add_sample(raw);
return mv / 1000.0f;
} }
int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; uint32_t final_value = aggr.aggregate();
if (this->channel1_ != ADC1_CHANNEL_MAX) { if (this->output_raw_) {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT); return final_value;
raw12 = adc1_get_raw(this->channel1_); }
if (raw12 < ADC_MAX) {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6); if (this->calibration_handle_ != nullptr) {
raw6 = adc1_get_raw(this->channel1_); int voltage_mv;
if (raw6 < ADC_MAX) { esp_err_t err = adc_cali_raw_to_voltage(this->calibration_handle_, final_value, &voltage_mv);
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5); if (err == ESP_OK) {
raw2 = adc1_get_raw(this->channel1_); return voltage_mv / 1000.0f;
if (raw2 < ADC_MAX) { } else {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0); ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
raw0 = adc1_get_raw(this->channel1_); if (this->calibration_handle_ != nullptr) {
} #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else // Other ESP32 variants use line fitting calibration
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
this->calibration_handle_ = nullptr;
} }
} }
} else if (this->channel2_ != ADC2_CHANNEL_MAX) { }
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_12_COMPAT);
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw12); return final_value * 3.3f / 4095.0f;
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); float ADCSensor::sample_autorange_() {
if (raw6 < ADC_MAX) { // Auto-range mode
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5); auto read_atten = [this](adc_atten_t atten) -> std::pair<int, float> {
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); // First reconfigure the attenuation for this reading
if (raw2 < ADC_MAX) { adc_oneshot_chan_cfg_t config = {
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0); .atten = atten,
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); .bitwidth = ADC_BITWIDTH_DEFAULT,
} };
esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error configuring ADC channel for autorange: %d", err);
return {-1, 0.0f};
}
// Need to recalibrate for the new attenuation
if (this->calibration_handle_ != nullptr) {
// Delete old calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif
this->calibration_handle_ = nullptr;
}
// Create new calibration handle for this attenuation
adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_curve_fitting_config_t cali_config = {};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
#endif
cali_config.unit_id = this->adc_unit_;
cali_config.atten = atten;
cali_config.bitwidth = ADC_BITWIDTH_DEFAULT;
err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
#else
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = atten,
.bitwidth = ADC_BITWIDTH_DEFAULT,
#if !defined(USE_ESP32_VARIANT_ESP32S2)
.default_vref = 1100,
#endif
};
err = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
#endif
int raw;
err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw);
if (err != ESP_OK) {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_delete_scheme_curve_fitting(handle);
#else
adc_cali_delete_scheme_line_fitting(handle);
#endif
}
return {-1, 0.0f};
}
float voltage = 0.0f;
if (handle != nullptr) {
int voltage_mv;
err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv);
if (err == ESP_OK) {
voltage = voltage_mv / 1000.0f;
} else {
voltage = raw * 3.3f / 4095.0f;
}
// Clean up calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_delete_scheme_curve_fitting(handle);
#else
adc_cali_delete_scheme_line_fitting(handle);
#endif
} else {
voltage = raw * 3.3f / 4095.0f;
}
return {raw, voltage};
};
auto [raw12, mv12] = read_atten(ADC_ATTEN_DB_12);
if (raw12 == -1) {
ESP_LOGE(TAG, "Failed to read ADC in autorange mode");
return NAN;
}
int raw6 = 4095, raw2 = 4095, raw0 = 4095;
float mv6 = 0, mv2 = 0, mv0 = 0;
if (raw12 < 4095) {
auto [raw6_val, mv6_val] = read_atten(ADC_ATTEN_DB_6);
raw6 = raw6_val;
mv6 = mv6_val;
if (raw6 < 4095 && raw6 != -1) {
auto [raw2_val, mv2_val] = read_atten(ADC_ATTEN_DB_2_5);
raw2 = raw2_val;
mv2 = mv2_val;
if (raw2 < 4095 && raw2 != -1) {
auto [raw0_val, mv0_val] = read_atten(ADC_ATTEN_DB_0);
raw0 = raw0_val;
mv0 = mv0_val;
} }
} }
} }
@ -155,19 +325,19 @@ float ADCSensor::sample() {
return NAN; return NAN;
} }
uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]); const int adc_half = 2048;
uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); uint32_t c12 = std::min(raw12, adc_half);
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); uint32_t c6 = adc_half - std::abs(raw6 - adc_half);
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); uint32_t c2 = adc_half - std::abs(raw2 - adc_half);
uint32_t c0 = std::min(4095 - raw0, adc_half);
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; uint32_t csum = c12 + c6 + c2 + c0;
uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); if (csum == 0) {
return mv_scaled / (float) (csum * 1000U); ESP_LOGE(TAG, "Invalid weight sum in autorange calculation");
return NAN;
}
return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum;
} }
} // namespace adc } // namespace adc

View File

@ -56,8 +56,6 @@ float ADCSensor::sample() {
return aggr.aggregate() / 1024.0f; return aggr.aggregate() / 1024.0f;
} }
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
} // namespace adc } // namespace adc
} // namespace esphome } // namespace esphome

View File

@ -10,13 +10,11 @@ from esphome.const import (
CONF_NUMBER, CONF_NUMBER,
CONF_PIN, CONF_PIN,
CONF_RAW, CONF_RAW,
CONF_WIFI,
DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_VOLT, UNIT_VOLT,
) )
from esphome.core import CORE from esphome.core import CORE
import esphome.final_validate as fv
from . import ( from . import (
ATTENUATION_MODES, ATTENUATION_MODES,
@ -24,6 +22,7 @@ from . import (
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
SAMPLING_MODES, SAMPLING_MODES,
adc_ns, adc_ns,
adc_unit_t,
validate_adc_pin, validate_adc_pin,
) )
@ -57,21 +56,6 @@ def validate_config(config):
return 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 = adc_ns.class_(
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
) )
@ -99,8 +83,6 @@ CONFIG_SCHEMA = cv.All(
validate_config, validate_config,
) )
FINAL_VALIDATE_SCHEMA = final_validate_config
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
@ -119,13 +101,13 @@ async def to_code(config):
cg.add(var.set_sample_count(config[CONF_SAMPLES])) cg.add(var.set_sample_count(config[CONF_SAMPLES]))
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto":
cg.add(var.set_autorange(cg.global_ns.true))
else:
cg.add(var.set_attenuation(attenuation))
if CORE.is_esp32: if CORE.is_esp32:
if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto":
cg.add(var.set_autorange(cg.global_ns.true))
else:
cg.add(var.set_attenuation(attenuation))
variant = get_esp32_variant() variant = get_esp32_variant()
pin_num = config[CONF_PIN][CONF_NUMBER] pin_num = config[CONF_PIN][CONF_NUMBER]
if ( if (
@ -133,10 +115,10 @@ async def to_code(config):
and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
): ):
chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
cg.add(var.set_channel1(chan)) cg.add(var.set_channel(adc_unit_t.ADC_UNIT_1, chan))
elif ( elif (
variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL
and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
): ):
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
cg.add(var.set_channel2(chan)) cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan))

View File

@ -222,37 +222,37 @@ message DeviceInfoResponse {
// The model of the board. For example NodeMCU // The model of the board. For example NodeMCU
string model = 6; string model = 6;
bool has_deep_sleep = 7; bool has_deep_sleep = 7 [(field_ifdef) = "USE_DEEP_SLEEP"];
// The esphome project details if set // The esphome project details if set
string project_name = 8; string project_name = 8 [(field_ifdef) = "ESPHOME_PROJECT_NAME"];
string project_version = 9; string project_version = 9 [(field_ifdef) = "ESPHOME_PROJECT_NAME"];
uint32 webserver_port = 10; uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"];
uint32 legacy_bluetooth_proxy_version = 11; uint32 legacy_bluetooth_proxy_version = 11 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
uint32 bluetooth_proxy_feature_flags = 15; uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
string manufacturer = 12; string manufacturer = 12;
string friendly_name = 13; string friendly_name = 13;
uint32 legacy_voice_assistant_version = 14; uint32 legacy_voice_assistant_version = 14 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
uint32 voice_assistant_feature_flags = 17; uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
string suggested_area = 16; string suggested_area = 16 [(field_ifdef) = "USE_AREAS"];
// The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA" // The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA"
string bluetooth_mac_address = 18; string bluetooth_mac_address = 18 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
// Supports receiving and saving api encryption key // Supports receiving and saving api encryption key
bool api_encryption_supported = 19; bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"];
repeated DeviceInfo devices = 20; repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES"];
repeated AreaInfo areas = 21; repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS"];
// Top-level area info to phase out suggested_area // Top-level area info to phase out suggested_area
AreaInfo area = 22; AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"];
} }
message ListEntitiesRequest { message ListEntitiesRequest {
@ -290,14 +290,14 @@ message ListEntitiesBinarySensorResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string device_class = 5; string device_class = 5;
bool is_status_binary_sensor = 6; bool is_status_binary_sensor = 6;
bool disabled_by_default = 7; bool disabled_by_default = 7;
string icon = 8; string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 9; EntityCategory entity_category = 9;
uint32 device_id = 10; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
} }
message BinarySensorStateResponse { message BinarySensorStateResponse {
option (id) = 21; option (id) = 21;
@ -311,7 +311,7 @@ message BinarySensorStateResponse {
// If the binary sensor does not have a valid state yet. // If the binary sensor does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3; bool missing_state = 3;
uint32 device_id = 4; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== COVER ==================== // ==================== COVER ====================
@ -324,17 +324,17 @@ message ListEntitiesCoverResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
bool assumed_state = 5; bool assumed_state = 5;
bool supports_position = 6; bool supports_position = 6;
bool supports_tilt = 7; bool supports_tilt = 7;
string device_class = 8; string device_class = 8;
bool disabled_by_default = 9; bool disabled_by_default = 9;
string icon = 10; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 11; EntityCategory entity_category = 11;
bool supports_stop = 12; bool supports_stop = 12;
uint32 device_id = 13; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
} }
enum LegacyCoverState { enum LegacyCoverState {
@ -361,7 +361,7 @@ message CoverStateResponse {
float position = 3; float position = 3;
float tilt = 4; float tilt = 4;
CoverOperation current_operation = 5; CoverOperation current_operation = 5;
uint32 device_id = 6; uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
} }
enum LegacyCoverCommand { enum LegacyCoverCommand {
@ -388,7 +388,7 @@ message CoverCommandRequest {
bool has_tilt = 6; bool has_tilt = 6;
float tilt = 7; float tilt = 7;
bool stop = 8; bool stop = 8;
uint32 device_id = 9; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== FAN ==================== // ==================== FAN ====================
@ -401,17 +401,17 @@ message ListEntitiesFanResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
bool supports_oscillation = 5; bool supports_oscillation = 5;
bool supports_speed = 6; bool supports_speed = 6;
bool supports_direction = 7; bool supports_direction = 7;
int32 supported_speed_count = 8; int32 supported_speed_count = 8;
bool disabled_by_default = 9; bool disabled_by_default = 9;
string icon = 10; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 11; EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12; repeated string supported_preset_modes = 12;
uint32 device_id = 13; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
} }
enum FanSpeed { enum FanSpeed {
FAN_SPEED_LOW = 0; FAN_SPEED_LOW = 0;
@ -436,7 +436,7 @@ message FanStateResponse {
FanDirection direction = 5; FanDirection direction = 5;
int32 speed_level = 6; int32 speed_level = 6;
string preset_mode = 7; string preset_mode = 7;
uint32 device_id = 8; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
} }
message FanCommandRequest { message FanCommandRequest {
option (id) = 31; option (id) = 31;
@ -458,7 +458,7 @@ message FanCommandRequest {
int32 speed_level = 11; int32 speed_level = 11;
bool has_preset_mode = 12; bool has_preset_mode = 12;
string preset_mode = 13; string preset_mode = 13;
uint32 device_id = 14; uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== LIGHT ==================== // ==================== LIGHT ====================
@ -484,7 +484,7 @@ message ListEntitiesLightResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
repeated ColorMode supported_color_modes = 12; repeated ColorMode supported_color_modes = 12;
// next four supports_* are for legacy clients, newer clients should use color modes // next four supports_* are for legacy clients, newer clients should use color modes
@ -496,9 +496,9 @@ message ListEntitiesLightResponse {
float max_mireds = 10; float max_mireds = 10;
repeated string effects = 11; repeated string effects = 11;
bool disabled_by_default = 13; bool disabled_by_default = 13;
string icon = 14; string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 15; EntityCategory entity_category = 15;
uint32 device_id = 16; uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"];
} }
message LightStateResponse { message LightStateResponse {
option (id) = 24; option (id) = 24;
@ -520,7 +520,7 @@ message LightStateResponse {
float cold_white = 12; float cold_white = 12;
float warm_white = 13; float warm_white = 13;
string effect = 9; string effect = 9;
uint32 device_id = 14; uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
} }
message LightCommandRequest { message LightCommandRequest {
option (id) = 32; option (id) = 32;
@ -556,7 +556,7 @@ message LightCommandRequest {
uint32 flash_length = 17; uint32 flash_length = 17;
bool has_effect = 18; bool has_effect = 18;
string effect = 19; string effect = 19;
uint32 device_id = 28; uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== SENSOR ==================== // ==================== SENSOR ====================
@ -582,9 +582,9 @@ message ListEntitiesSensorResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string unit_of_measurement = 6; string unit_of_measurement = 6;
int32 accuracy_decimals = 7; int32 accuracy_decimals = 7;
bool force_update = 8; bool force_update = 8;
@ -594,7 +594,7 @@ message ListEntitiesSensorResponse {
SensorLastResetType legacy_last_reset_type = 11; SensorLastResetType legacy_last_reset_type = 11;
bool disabled_by_default = 12; bool disabled_by_default = 12;
EntityCategory entity_category = 13; EntityCategory entity_category = 13;
uint32 device_id = 14; uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
} }
message SensorStateResponse { message SensorStateResponse {
option (id) = 25; option (id) = 25;
@ -608,7 +608,7 @@ message SensorStateResponse {
// If the sensor does not have a valid state yet. // If the sensor does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3; bool missing_state = 3;
uint32 device_id = 4; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== SWITCH ==================== // ==================== SWITCH ====================
@ -621,14 +621,14 @@ message ListEntitiesSwitchResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool assumed_state = 6; bool assumed_state = 6;
bool disabled_by_default = 7; bool disabled_by_default = 7;
EntityCategory entity_category = 8; EntityCategory entity_category = 8;
string device_class = 9; string device_class = 9;
uint32 device_id = 10; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
} }
message SwitchStateResponse { message SwitchStateResponse {
option (id) = 26; option (id) = 26;
@ -639,7 +639,7 @@ message SwitchStateResponse {
fixed32 key = 1; fixed32 key = 1;
bool state = 2; bool state = 2;
uint32 device_id = 3; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
} }
message SwitchCommandRequest { message SwitchCommandRequest {
option (id) = 33; option (id) = 33;
@ -650,7 +650,7 @@ message SwitchCommandRequest {
fixed32 key = 1; fixed32 key = 1;
bool state = 2; bool state = 2;
uint32 device_id = 3; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== TEXT SENSOR ==================== // ==================== TEXT SENSOR ====================
@ -663,13 +663,13 @@ message ListEntitiesTextSensorResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
string device_class = 8; string device_class = 8;
uint32 device_id = 9; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
} }
message TextSensorStateResponse { message TextSensorStateResponse {
option (id) = 27; option (id) = 27;
@ -683,7 +683,7 @@ message TextSensorStateResponse {
// If the text sensor does not have a valid state yet. // If the text sensor does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3; bool missing_state = 3;
uint32 device_id = 4; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== SUBSCRIBE LOGS ==================== // ==================== SUBSCRIBE LOGS ====================
@ -853,11 +853,11 @@ message ListEntitiesCameraResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
bool disabled_by_default = 5; bool disabled_by_default = 5;
string icon = 6; string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
uint32 device_id = 8; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
} }
message CameraImageResponse { message CameraImageResponse {
@ -869,7 +869,7 @@ message CameraImageResponse {
fixed32 key = 1; fixed32 key = 1;
bytes data = 2; bytes data = 2;
bool done = 3; bool done = 3;
uint32 device_id = 4; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
} }
message CameraImageRequest { message CameraImageRequest {
option (id) = 45; option (id) = 45;
@ -937,7 +937,7 @@ message ListEntitiesClimateResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
bool supports_current_temperature = 5; bool supports_current_temperature = 5;
bool supports_two_point_target_temperature = 6; bool supports_two_point_target_temperature = 6;
@ -955,14 +955,14 @@ message ListEntitiesClimateResponse {
repeated ClimatePreset supported_presets = 16; repeated ClimatePreset supported_presets = 16;
repeated string supported_custom_presets = 17; repeated string supported_custom_presets = 17;
bool disabled_by_default = 18; bool disabled_by_default = 18;
string icon = 19; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20; EntityCategory entity_category = 20;
float visual_current_temperature_step = 21; float visual_current_temperature_step = 21;
bool supports_current_humidity = 22; bool supports_current_humidity = 22;
bool supports_target_humidity = 23; bool supports_target_humidity = 23;
float visual_min_humidity = 24; float visual_min_humidity = 24;
float visual_max_humidity = 25; float visual_max_humidity = 25;
uint32 device_id = 26; uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
} }
message ClimateStateResponse { message ClimateStateResponse {
option (id) = 47; option (id) = 47;
@ -987,7 +987,7 @@ message ClimateStateResponse {
string custom_preset = 13; string custom_preset = 13;
float current_humidity = 14; float current_humidity = 14;
float target_humidity = 15; float target_humidity = 15;
uint32 device_id = 16; uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"];
} }
message ClimateCommandRequest { message ClimateCommandRequest {
option (id) = 48; option (id) = 48;
@ -1020,7 +1020,7 @@ message ClimateCommandRequest {
string custom_preset = 21; string custom_preset = 21;
bool has_target_humidity = 22; bool has_target_humidity = 22;
float target_humidity = 23; float target_humidity = 23;
uint32 device_id = 24; uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== NUMBER ==================== // ==================== NUMBER ====================
@ -1038,9 +1038,9 @@ message ListEntitiesNumberResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
float min_value = 6; float min_value = 6;
float max_value = 7; float max_value = 7;
float step = 8; float step = 8;
@ -1049,7 +1049,7 @@ message ListEntitiesNumberResponse {
string unit_of_measurement = 11; string unit_of_measurement = 11;
NumberMode mode = 12; NumberMode mode = 12;
string device_class = 13; string device_class = 13;
uint32 device_id = 14; uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
} }
message NumberStateResponse { message NumberStateResponse {
option (id) = 50; option (id) = 50;
@ -1063,7 +1063,7 @@ message NumberStateResponse {
// If the number does not have a valid state yet. // If the number does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3; bool missing_state = 3;
uint32 device_id = 4; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
} }
message NumberCommandRequest { message NumberCommandRequest {
option (id) = 51; option (id) = 51;
@ -1074,7 +1074,7 @@ message NumberCommandRequest {
fixed32 key = 1; fixed32 key = 1;
float state = 2; float state = 2;
uint32 device_id = 3; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== SELECT ==================== // ==================== SELECT ====================
@ -1087,13 +1087,13 @@ message ListEntitiesSelectResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
repeated string options = 6; repeated string options = 6;
bool disabled_by_default = 7; bool disabled_by_default = 7;
EntityCategory entity_category = 8; EntityCategory entity_category = 8;
uint32 device_id = 9; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
} }
message SelectStateResponse { message SelectStateResponse {
option (id) = 53; option (id) = 53;
@ -1107,7 +1107,7 @@ message SelectStateResponse {
// If the select does not have a valid state yet. // If the select does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3; bool missing_state = 3;
uint32 device_id = 4; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
} }
message SelectCommandRequest { message SelectCommandRequest {
option (id) = 54; option (id) = 54;
@ -1118,7 +1118,7 @@ message SelectCommandRequest {
fixed32 key = 1; fixed32 key = 1;
string state = 2; string state = 2;
uint32 device_id = 3; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== SIREN ==================== // ==================== SIREN ====================
@ -1131,15 +1131,15 @@ message ListEntitiesSirenResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
repeated string tones = 7; repeated string tones = 7;
bool supports_duration = 8; bool supports_duration = 8;
bool supports_volume = 9; bool supports_volume = 9;
EntityCategory entity_category = 10; EntityCategory entity_category = 10;
uint32 device_id = 11; uint32 device_id = 11 [(field_ifdef) = "USE_DEVICES"];
} }
message SirenStateResponse { message SirenStateResponse {
option (id) = 56; option (id) = 56;
@ -1150,7 +1150,7 @@ message SirenStateResponse {
fixed32 key = 1; fixed32 key = 1;
bool state = 2; bool state = 2;
uint32 device_id = 3; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
} }
message SirenCommandRequest { message SirenCommandRequest {
option (id) = 57; option (id) = 57;
@ -1168,7 +1168,7 @@ message SirenCommandRequest {
uint32 duration = 7; uint32 duration = 7;
bool has_volume = 8; bool has_volume = 8;
float volume = 9; float volume = 9;
uint32 device_id = 10; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== LOCK ==================== // ==================== LOCK ====================
@ -1194,9 +1194,9 @@ message ListEntitiesLockResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
bool assumed_state = 8; bool assumed_state = 8;
@ -1206,7 +1206,7 @@ message ListEntitiesLockResponse {
// Not yet implemented: // Not yet implemented:
string code_format = 11; string code_format = 11;
uint32 device_id = 12; uint32 device_id = 12 [(field_ifdef) = "USE_DEVICES"];
} }
message LockStateResponse { message LockStateResponse {
option (id) = 59; option (id) = 59;
@ -1216,7 +1216,7 @@ message LockStateResponse {
option (no_delay) = true; option (no_delay) = true;
fixed32 key = 1; fixed32 key = 1;
LockState state = 2; LockState state = 2;
uint32 device_id = 3; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
} }
message LockCommandRequest { message LockCommandRequest {
option (id) = 60; option (id) = 60;
@ -1230,7 +1230,7 @@ message LockCommandRequest {
// Not yet implemented: // Not yet implemented:
bool has_code = 3; bool has_code = 3;
string code = 4; string code = 4;
uint32 device_id = 5; uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== BUTTON ==================== // ==================== BUTTON ====================
@ -1243,13 +1243,13 @@ message ListEntitiesButtonResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
string device_class = 8; string device_class = 8;
uint32 device_id = 9; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
} }
message ButtonCommandRequest { message ButtonCommandRequest {
option (id) = 62; option (id) = 62;
@ -1259,7 +1259,7 @@ message ButtonCommandRequest {
option (base_class) = "CommandProtoMessage"; option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
uint32 device_id = 2; uint32 device_id = 2 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== MEDIA PLAYER ==================== // ==================== MEDIA PLAYER ====================
@ -1298,9 +1298,9 @@ message ListEntitiesMediaPlayerResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
@ -1308,7 +1308,7 @@ message ListEntitiesMediaPlayerResponse {
repeated MediaPlayerSupportedFormat supported_formats = 9; repeated MediaPlayerSupportedFormat supported_formats = 9;
uint32 device_id = 10; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
} }
message MediaPlayerStateResponse { message MediaPlayerStateResponse {
option (id) = 64; option (id) = 64;
@ -1320,7 +1320,7 @@ message MediaPlayerStateResponse {
MediaPlayerState state = 2; MediaPlayerState state = 2;
float volume = 3; float volume = 3;
bool muted = 4; bool muted = 4;
uint32 device_id = 5; uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
} }
message MediaPlayerCommandRequest { message MediaPlayerCommandRequest {
option (id) = 65; option (id) = 65;
@ -1342,7 +1342,7 @@ message MediaPlayerCommandRequest {
bool has_announcement = 8; bool has_announcement = 8;
bool announcement = 9; bool announcement = 9;
uint32 device_id = 10; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== BLUETOOTH ==================== // ==================== BLUETOOTH ====================
@ -1381,7 +1381,7 @@ message BluetoothLERawAdvertisement {
sint32 rssi = 2; sint32 rssi = 2;
uint32 address_type = 3; uint32 address_type = 3;
bytes data = 4; bytes data = 4 [(fixed_array_size) = 62];
} }
message BluetoothLERawAdvertisementsResponse { message BluetoothLERawAdvertisementsResponse {
@ -1845,14 +1845,14 @@ message ListEntitiesAlarmControlPanelResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
uint32 supported_features = 8; uint32 supported_features = 8;
bool requires_code = 9; bool requires_code = 9;
bool requires_code_to_arm = 10; bool requires_code_to_arm = 10;
uint32 device_id = 11; uint32 device_id = 11 [(field_ifdef) = "USE_DEVICES"];
} }
message AlarmControlPanelStateResponse { message AlarmControlPanelStateResponse {
@ -1863,7 +1863,7 @@ message AlarmControlPanelStateResponse {
option (no_delay) = true; option (no_delay) = true;
fixed32 key = 1; fixed32 key = 1;
AlarmControlPanelState state = 2; AlarmControlPanelState state = 2;
uint32 device_id = 3; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
} }
message AlarmControlPanelCommandRequest { message AlarmControlPanelCommandRequest {
@ -1875,7 +1875,7 @@ message AlarmControlPanelCommandRequest {
fixed32 key = 1; fixed32 key = 1;
AlarmControlPanelStateCommand command = 2; AlarmControlPanelStateCommand command = 2;
string code = 3; string code = 3;
uint32 device_id = 4; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
} }
// ===================== TEXT ===================== // ===================== TEXT =====================
@ -1892,8 +1892,8 @@ message ListEntitiesTextResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
@ -1901,7 +1901,7 @@ message ListEntitiesTextResponse {
uint32 max_length = 9; uint32 max_length = 9;
string pattern = 10; string pattern = 10;
TextMode mode = 11; TextMode mode = 11;
uint32 device_id = 12; uint32 device_id = 12 [(field_ifdef) = "USE_DEVICES"];
} }
message TextStateResponse { message TextStateResponse {
option (id) = 98; option (id) = 98;
@ -1915,7 +1915,7 @@ message TextStateResponse {
// If the Text does not have a valid state yet. // If the Text does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3; bool missing_state = 3;
uint32 device_id = 4; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
} }
message TextCommandRequest { message TextCommandRequest {
option (id) = 99; option (id) = 99;
@ -1926,7 +1926,7 @@ message TextCommandRequest {
fixed32 key = 1; fixed32 key = 1;
string state = 2; string state = 2;
uint32 device_id = 3; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
} }
@ -1940,12 +1940,12 @@ message ListEntitiesDateResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
uint32 device_id = 8; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
} }
message DateStateResponse { message DateStateResponse {
option (id) = 101; option (id) = 101;
@ -1961,7 +1961,7 @@ message DateStateResponse {
uint32 year = 3; uint32 year = 3;
uint32 month = 4; uint32 month = 4;
uint32 day = 5; uint32 day = 5;
uint32 device_id = 6; uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
} }
message DateCommandRequest { message DateCommandRequest {
option (id) = 102; option (id) = 102;
@ -1974,7 +1974,7 @@ message DateCommandRequest {
uint32 year = 2; uint32 year = 2;
uint32 month = 3; uint32 month = 3;
uint32 day = 4; uint32 day = 4;
uint32 device_id = 5; uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== DATETIME TIME ==================== // ==================== DATETIME TIME ====================
@ -1987,12 +1987,12 @@ message ListEntitiesTimeResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
uint32 device_id = 8; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
} }
message TimeStateResponse { message TimeStateResponse {
option (id) = 104; option (id) = 104;
@ -2008,7 +2008,7 @@ message TimeStateResponse {
uint32 hour = 3; uint32 hour = 3;
uint32 minute = 4; uint32 minute = 4;
uint32 second = 5; uint32 second = 5;
uint32 device_id = 6; uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
} }
message TimeCommandRequest { message TimeCommandRequest {
option (id) = 105; option (id) = 105;
@ -2021,7 +2021,7 @@ message TimeCommandRequest {
uint32 hour = 2; uint32 hour = 2;
uint32 minute = 3; uint32 minute = 3;
uint32 second = 4; uint32 second = 4;
uint32 device_id = 5; uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== EVENT ==================== // ==================== EVENT ====================
@ -2034,15 +2034,15 @@ message ListEntitiesEventResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
string device_class = 8; string device_class = 8;
repeated string event_types = 9; repeated string event_types = 9;
uint32 device_id = 10; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
} }
message EventResponse { message EventResponse {
option (id) = 108; option (id) = 108;
@ -2052,7 +2052,7 @@ message EventResponse {
fixed32 key = 1; fixed32 key = 1;
string event_type = 2; string event_type = 2;
uint32 device_id = 3; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== VALVE ==================== // ==================== VALVE ====================
@ -2065,9 +2065,9 @@ message ListEntitiesValveResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
string device_class = 8; string device_class = 8;
@ -2075,7 +2075,7 @@ message ListEntitiesValveResponse {
bool assumed_state = 9; bool assumed_state = 9;
bool supports_position = 10; bool supports_position = 10;
bool supports_stop = 11; bool supports_stop = 11;
uint32 device_id = 12; uint32 device_id = 12 [(field_ifdef) = "USE_DEVICES"];
} }
enum ValveOperation { enum ValveOperation {
@ -2093,7 +2093,7 @@ message ValveStateResponse {
fixed32 key = 1; fixed32 key = 1;
float position = 2; float position = 2;
ValveOperation current_operation = 3; ValveOperation current_operation = 3;
uint32 device_id = 4; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
} }
message ValveCommandRequest { message ValveCommandRequest {
@ -2107,7 +2107,7 @@ message ValveCommandRequest {
bool has_position = 2; bool has_position = 2;
float position = 3; float position = 3;
bool stop = 4; bool stop = 4;
uint32 device_id = 5; uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== DATETIME DATETIME ==================== // ==================== DATETIME DATETIME ====================
@ -2120,12 +2120,12 @@ message ListEntitiesDateTimeResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
uint32 device_id = 8; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
} }
message DateTimeStateResponse { message DateTimeStateResponse {
option (id) = 113; option (id) = 113;
@ -2139,7 +2139,7 @@ message DateTimeStateResponse {
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 2; bool missing_state = 2;
fixed32 epoch_seconds = 3; fixed32 epoch_seconds = 3;
uint32 device_id = 4; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
} }
message DateTimeCommandRequest { message DateTimeCommandRequest {
option (id) = 114; option (id) = 114;
@ -2150,7 +2150,7 @@ message DateTimeCommandRequest {
fixed32 key = 1; fixed32 key = 1;
fixed32 epoch_seconds = 2; fixed32 epoch_seconds = 2;
uint32 device_id = 3; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== UPDATE ==================== // ==================== UPDATE ====================
@ -2163,13 +2163,13 @@ message ListEntitiesUpdateResponse {
string object_id = 1; string object_id = 1;
fixed32 key = 2; fixed32 key = 2;
string name = 3; string name = 3;
string unique_id = 4; reserved 4; // Deprecated: was string unique_id
string icon = 5; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
string device_class = 8; string device_class = 8;
uint32 device_id = 9; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
} }
message UpdateStateResponse { message UpdateStateResponse {
option (id) = 117; option (id) = 117;
@ -2188,7 +2188,7 @@ message UpdateStateResponse {
string title = 8; string title = 8;
string release_summary = 9; string release_summary = 9;
string release_url = 10; string release_url = 10;
uint32 device_id = 11; uint32 device_id = 11 [(field_ifdef) = "USE_DEVICES"];
} }
enum UpdateCommand { enum UpdateCommand {
UPDATE_COMMAND_NONE = 0; UPDATE_COMMAND_NONE = 0;
@ -2204,5 +2204,5 @@ message UpdateCommandRequest {
fixed32 key = 1; fixed32 key = 1;
UpdateCommand command = 2; UpdateCommand command = 2;
uint32 device_id = 3; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
} }

View File

@ -42,18 +42,37 @@ static const char *const TAG = "api.connection";
static const int CAMERA_STOP_STREAM = 5000; static const int CAMERA_STOP_STREAM = 5000;
#endif #endif
// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call object #ifdef USE_DEVICES
// Helper macro for entity command handlers - gets entity by key and device_id, returns if not found, and creates call
// object
#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \
if ((entity_var) == nullptr) \
return; \
auto call = (entity_var)->make_call();
// Helper macro for entity command handlers that don't use make_call() - gets entity by key and device_id and returns if
// not found
#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \
if ((entity_var) == nullptr) \
return;
#else // No device support, use simpler macros
// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call
// object
#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \ #define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
if ((entity_var) == nullptr) \ if ((entity_var) == nullptr) \
return; \ return; \
auto call = (entity_var)->make_call(); auto call = (entity_var)->make_call();
// Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if not found // Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if
// not found
#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \ #define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \ entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
if ((entity_var) == nullptr) \ if ((entity_var) == nullptr) \
return; return;
#endif // USE_DEVICES
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
@ -86,8 +105,8 @@ void APIConnection::start() {
APIError err = this->helper_->init(); APIError err = this->helper_->init();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(), ESP_LOGW(TAG, "%s: Helper init failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
api_error_to_str(err), errno); errno);
return; return;
} }
this->client_info_ = helper_->getpeername(); this->client_info_ = helper_->getpeername();
@ -119,7 +138,7 @@ void APIConnection::loop() {
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno); api_error_to_str(err), errno);
return; return;
} }
@ -136,14 +155,8 @@ void APIConnection::loop() {
break; break;
} else if (err != APIError::OK) { } else if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { ESP_LOGW(TAG, "%s: Reading failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); errno);
} else if (err == APIError::CONNECTION_CLOSED) {
ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str());
} else {
ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno);
}
return; return;
} else { } else {
this->last_traffic_ = now; this->last_traffic_ = now;
@ -186,9 +199,11 @@ void APIConnection::loop() {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str());
} }
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) {
// Only send ping if we're not disconnecting
ESP_LOGVV(TAG, "Sending keepalive PING"); ESP_LOGVV(TAG, "Sending keepalive PING");
this->flags_.sent_ping = this->send_message(PingRequest()); PingRequest req;
this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE);
if (!this->flags_.sent_ping) { if (!this->flags_.sent_ping) {
// If we can't send the ping request directly (tx_buffer full), // If we can't send the ping request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority // schedule it at the front of the batch so it will be sent with priority
@ -237,7 +252,7 @@ void APIConnection::loop() {
resp.entity_id = it.entity_id; resp.entity_id = it.entity_id;
resp.attribute = it.attribute.value(); resp.attribute = it.attribute.value();
resp.once = it.once; resp.once = it.once;
if (this->send_message(resp)) { if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
state_subs_at_++; state_subs_at_++;
} }
} else { } else {
@ -246,10 +261,6 @@ void APIConnection::loop() {
} }
} }
std::string get_default_unique_id(const std::string &component_type, EntityBase *entity) {
return App.get_name() + component_type + entity->get_object_id();
}
DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) {
// remote initiated disconnect_client // remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response // don't close yet, we still need to send the disconnect response
@ -326,8 +337,8 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn
BinarySensorStateResponse resp; BinarySensorStateResponse resp;
resp.state = binary_sensor->state; resp.state = binary_sensor->state;
resp.missing_state = !binary_sensor->has_state(); resp.missing_state = !binary_sensor->has_state();
fill_entity_state_base(binary_sensor, resp); return fill_and_encode_entity_state(binary_sensor, resp, BinarySensorStateResponse::MESSAGE_TYPE, conn,
return encode_message_to_buffer(resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); remaining_size, is_single);
} }
uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -336,9 +347,8 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne
ListEntitiesBinarySensorResponse msg; ListEntitiesBinarySensorResponse msg;
msg.device_class = binary_sensor->get_device_class(); msg.device_class = binary_sensor->get_device_class();
msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor();
msg.unique_id = get_default_unique_id("binary_sensor", binary_sensor); return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn,
fill_entity_info_base(binary_sensor, msg); remaining_size, is_single);
return encode_message_to_buffer(msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
#endif #endif
@ -358,8 +368,7 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *
if (traits.get_supports_tilt()) if (traits.get_supports_tilt())
msg.tilt = cover->tilt; msg.tilt = cover->tilt;
msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation); msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation);
fill_entity_state_base(cover, msg); return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -371,9 +380,8 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
msg.supports_tilt = traits.get_supports_tilt(); msg.supports_tilt = traits.get_supports_tilt();
msg.supports_stop = traits.get_supports_stop(); msg.supports_stop = traits.get_supports_stop();
msg.device_class = cover->get_device_class(); msg.device_class = cover->get_device_class();
msg.unique_id = get_default_unique_id("cover", cover); return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(cover, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::cover_command(const CoverCommandRequest &msg) { void APIConnection::cover_command(const CoverCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
@ -420,8 +428,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
msg.direction = static_cast<enums::FanDirection>(fan->direction); msg.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes()) if (traits.supports_preset_modes())
msg.preset_mode = fan->preset_mode; msg.preset_mode = fan->preset_mode;
fill_entity_state_base(fan, msg); return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -434,9 +441,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
msg.supported_speed_count = traits.supported_speed_count(); msg.supported_speed_count = traits.supported_speed_count();
for (auto const &preset : traits.supported_preset_modes()) for (auto const &preset : traits.supported_preset_modes())
msg.supported_preset_modes.push_back(preset); msg.supported_preset_modes.push_back(preset);
msg.unique_id = get_default_unique_id("fan", fan); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
fill_entity_info_base(fan, msg);
return encode_message_to_buffer(msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::fan_command(const FanCommandRequest &msg) { void APIConnection::fan_command(const FanCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
@ -481,8 +486,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
resp.warm_white = values.get_warm_white(); resp.warm_white = values.get_warm_white();
if (light->supports_effects()) if (light->supports_effects())
resp.effect = light->get_effect_name(); resp.effect = light->get_effect_name();
fill_entity_state_base(light, resp); return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -508,9 +512,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
msg.effects.push_back(effect->get_name()); msg.effects.push_back(effect->get_name());
} }
} }
msg.unique_id = get_default_unique_id("light", light); return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(light, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::light_command(const LightCommandRequest &msg) { void APIConnection::light_command(const LightCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
@ -557,8 +560,7 @@ uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection
SensorStateResponse resp; SensorStateResponse resp;
resp.state = sensor->state; resp.state = sensor->state;
resp.missing_state = !sensor->has_state(); resp.missing_state = !sensor->has_state();
fill_entity_state_base(sensor, resp); return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -570,11 +572,8 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *
msg.force_update = sensor->get_force_update(); msg.force_update = sensor->get_force_update();
msg.device_class = sensor->get_device_class(); msg.device_class = sensor->get_device_class();
msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class()); msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class());
msg.unique_id = sensor->unique_id(); return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size,
if (msg.unique_id.empty()) is_single);
msg.unique_id = get_default_unique_id("sensor", sensor);
fill_entity_info_base(sensor, msg);
return encode_message_to_buffer(msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
#endif #endif
@ -589,8 +588,8 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection
auto *a_switch = static_cast<switch_::Switch *>(entity); auto *a_switch = static_cast<switch_::Switch *>(entity);
SwitchStateResponse resp; SwitchStateResponse resp;
resp.state = a_switch->state; resp.state = a_switch->state;
fill_entity_state_base(a_switch, resp); return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size,
return encode_message_to_buffer(resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); is_single);
} }
uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -599,9 +598,8 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
ListEntitiesSwitchResponse msg; ListEntitiesSwitchResponse msg;
msg.assumed_state = a_switch->assumed_state(); msg.assumed_state = a_switch->assumed_state();
msg.device_class = a_switch->get_device_class(); msg.device_class = a_switch->get_device_class();
msg.unique_id = get_default_unique_id("switch", a_switch); return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(a_switch, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::switch_command(const SwitchCommandRequest &msg) { void APIConnection::switch_command(const SwitchCommandRequest &msg) {
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
@ -626,19 +624,16 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec
TextSensorStateResponse resp; TextSensorStateResponse resp;
resp.state = text_sensor->state; resp.state = text_sensor->state;
resp.missing_state = !text_sensor->has_state(); resp.missing_state = !text_sensor->has_state();
fill_entity_state_base(text_sensor, resp); return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size,
return encode_message_to_buffer(resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); is_single);
} }
uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
ListEntitiesTextSensorResponse msg; ListEntitiesTextSensorResponse msg;
msg.device_class = text_sensor->get_device_class(); msg.device_class = text_sensor->get_device_class();
msg.unique_id = text_sensor->unique_id(); return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn,
if (msg.unique_id.empty()) remaining_size, is_single);
msg.unique_id = get_default_unique_id("text_sensor", text_sensor);
fill_entity_info_base(text_sensor, msg);
return encode_message_to_buffer(msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
#endif #endif
@ -651,7 +646,6 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
bool is_single) { bool is_single) {
auto *climate = static_cast<climate::Climate *>(entity); auto *climate = static_cast<climate::Climate *>(entity);
ClimateStateResponse resp; ClimateStateResponse resp;
fill_entity_state_base(climate, resp);
auto traits = climate->get_traits(); auto traits = climate->get_traits();
resp.mode = static_cast<enums::ClimateMode>(climate->mode); resp.mode = static_cast<enums::ClimateMode>(climate->mode);
resp.action = static_cast<enums::ClimateAction>(climate->action); resp.action = static_cast<enums::ClimateAction>(climate->action);
@ -678,7 +672,8 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
resp.current_humidity = climate->current_humidity; resp.current_humidity = climate->current_humidity;
if (traits.get_supports_target_humidity()) if (traits.get_supports_target_humidity())
resp.target_humidity = climate->target_humidity; resp.target_humidity = climate->target_humidity;
return encode_message_to_buffer(resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size,
is_single);
} }
uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -709,9 +704,8 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.supported_custom_presets.push_back(custom_preset); msg.supported_custom_presets.push_back(custom_preset);
for (auto swing_mode : traits.get_supported_swing_modes()) for (auto swing_mode : traits.get_supported_swing_modes())
msg.supported_swing_modes.push_back(static_cast<enums::ClimateSwingMode>(swing_mode)); msg.supported_swing_modes.push_back(static_cast<enums::ClimateSwingMode>(swing_mode));
msg.unique_id = get_default_unique_id("climate", climate); return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(climate, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::climate_command(const ClimateCommandRequest &msg) { void APIConnection::climate_command(const ClimateCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
@ -751,8 +745,7 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection
NumberStateResponse resp; NumberStateResponse resp;
resp.state = number->state; resp.state = number->state;
resp.missing_state = !number->has_state(); resp.missing_state = !number->has_state();
fill_entity_state_base(number, resp); return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -765,9 +758,8 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
msg.min_value = number->traits.get_min_value(); msg.min_value = number->traits.get_min_value();
msg.max_value = number->traits.get_max_value(); msg.max_value = number->traits.get_max_value();
msg.step = number->traits.get_step(); msg.step = number->traits.get_step();
msg.unique_id = get_default_unique_id("number", number); return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(number, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::number_command(const NumberCommandRequest &msg) { void APIConnection::number_command(const NumberCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
@ -789,16 +781,14 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c
resp.year = date->year; resp.year = date->year;
resp.month = date->month; resp.month = date->month;
resp.day = date->day; resp.day = date->day;
fill_entity_state_base(date, resp); return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
auto *date = static_cast<datetime::DateEntity *>(entity); auto *date = static_cast<datetime::DateEntity *>(entity);
ListEntitiesDateResponse msg; ListEntitiesDateResponse msg;
msg.unique_id = get_default_unique_id("date", date); return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(date, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::date_command(const DateCommandRequest &msg) { void APIConnection::date_command(const DateCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
@ -820,16 +810,14 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c
resp.hour = time->hour; resp.hour = time->hour;
resp.minute = time->minute; resp.minute = time->minute;
resp.second = time->second; resp.second = time->second;
fill_entity_state_base(time, resp); return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
auto *time = static_cast<datetime::TimeEntity *>(entity); auto *time = static_cast<datetime::TimeEntity *>(entity);
ListEntitiesTimeResponse msg; ListEntitiesTimeResponse msg;
msg.unique_id = get_default_unique_id("time", time); return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(time, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::time_command(const TimeCommandRequest &msg) { void APIConnection::time_command(const TimeCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
@ -852,16 +840,15 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio
ESPTime state = datetime->state_as_esptime(); ESPTime state = datetime->state_as_esptime();
resp.epoch_seconds = state.timestamp; resp.epoch_seconds = state.timestamp;
} }
fill_entity_state_base(datetime, resp); return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size,
return encode_message_to_buffer(resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); is_single);
} }
uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
auto *datetime = static_cast<datetime::DateTimeEntity *>(entity); auto *datetime = static_cast<datetime::DateTimeEntity *>(entity);
ListEntitiesDateTimeResponse msg; ListEntitiesDateTimeResponse msg;
msg.unique_id = get_default_unique_id("datetime", datetime); return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(datetime, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
@ -882,8 +869,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c
TextStateResponse resp; TextStateResponse resp;
resp.state = text->state; resp.state = text->state;
resp.missing_state = !text->has_state(); resp.missing_state = !text->has_state();
fill_entity_state_base(text, resp); return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -894,9 +880,8 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
msg.min_length = text->traits.get_min_length(); msg.min_length = text->traits.get_min_length();
msg.max_length = text->traits.get_max_length(); msg.max_length = text->traits.get_max_length();
msg.pattern = text->traits.get_pattern(); msg.pattern = text->traits.get_pattern();
msg.unique_id = get_default_unique_id("text", text); return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(text, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::text_command(const TextCommandRequest &msg) { void APIConnection::text_command(const TextCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
@ -917,8 +902,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
SelectStateResponse resp; SelectStateResponse resp;
resp.state = select->state; resp.state = select->state;
resp.missing_state = !select->has_state(); resp.missing_state = !select->has_state();
fill_entity_state_base(select, resp); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -927,9 +911,8 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
ListEntitiesSelectResponse msg; ListEntitiesSelectResponse msg;
for (const auto &option : select->traits.get_options()) for (const auto &option : select->traits.get_options())
msg.options.push_back(option); msg.options.push_back(option);
msg.unique_id = get_default_unique_id("select", select); return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(select, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::select_command(const SelectCommandRequest &msg) { void APIConnection::select_command(const SelectCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
@ -944,9 +927,8 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
auto *button = static_cast<button::Button *>(entity); auto *button = static_cast<button::Button *>(entity);
ListEntitiesButtonResponse msg; ListEntitiesButtonResponse msg;
msg.device_class = button->get_device_class(); msg.device_class = button->get_device_class();
msg.unique_id = get_default_unique_id("button", button); return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(button, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) {
ENTITY_COMMAND_GET(button::Button, button, button) ENTITY_COMMAND_GET(button::Button, button, button)
@ -965,8 +947,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c
auto *a_lock = static_cast<lock::Lock *>(entity); auto *a_lock = static_cast<lock::Lock *>(entity);
LockStateResponse resp; LockStateResponse resp;
resp.state = static_cast<enums::LockState>(a_lock->state); resp.state = static_cast<enums::LockState>(a_lock->state);
fill_entity_state_base(a_lock, resp); return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -976,9 +957,8 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co
msg.assumed_state = a_lock->traits.get_assumed_state(); msg.assumed_state = a_lock->traits.get_assumed_state();
msg.supports_open = a_lock->traits.get_supports_open(); msg.supports_open = a_lock->traits.get_supports_open();
msg.requires_code = a_lock->traits.get_requires_code(); msg.requires_code = a_lock->traits.get_requires_code();
msg.unique_id = get_default_unique_id("lock", a_lock); return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(a_lock, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::lock_command(const LockCommandRequest &msg) { void APIConnection::lock_command(const LockCommandRequest &msg) {
ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
@ -1008,8 +988,7 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *
ValveStateResponse resp; ValveStateResponse resp;
resp.position = valve->position; resp.position = valve->position;
resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation); resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation);
fill_entity_state_base(valve, resp); return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -1020,9 +999,8 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
msg.assumed_state = traits.get_is_assumed_state(); msg.assumed_state = traits.get_is_assumed_state();
msg.supports_position = traits.get_supports_position(); msg.supports_position = traits.get_supports_position();
msg.supports_stop = traits.get_supports_stop(); msg.supports_stop = traits.get_supports_stop();
msg.unique_id = get_default_unique_id("valve", valve); return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(valve, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::valve_command(const ValveCommandRequest &msg) { void APIConnection::valve_command(const ValveCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
@ -1049,8 +1027,8 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne
resp.state = static_cast<enums::MediaPlayerState>(report_state); resp.state = static_cast<enums::MediaPlayerState>(report_state);
resp.volume = media_player->volume; resp.volume = media_player->volume;
resp.muted = media_player->is_muted(); resp.muted = media_player->is_muted();
fill_entity_state_base(media_player, resp); return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size,
return encode_message_to_buffer(resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); is_single);
} }
uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@ -1067,9 +1045,8 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
media_format.sample_bytes = supported_format.sample_bytes; media_format.sample_bytes = supported_format.sample_bytes;
msg.supported_formats.push_back(media_format); msg.supported_formats.push_back(media_format);
} }
msg.unique_id = get_default_unique_id("media_player", media_player); return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn,
fill_entity_info_base(media_player, msg); remaining_size, is_single);
return encode_message_to_buffer(msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
@ -1104,9 +1081,8 @@ uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *
bool is_single) { bool is_single) {
auto *camera = static_cast<camera::Camera *>(entity); auto *camera = static_cast<camera::Camera *>(entity);
ListEntitiesCameraResponse msg; ListEntitiesCameraResponse msg;
msg.unique_id = get_default_unique_id("camera", camera); return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(camera, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::camera_image(const CameraImageRequest &msg) { void APIConnection::camera_image(const CameraImageRequest &msg) {
if (camera::Camera::instance() == nullptr) if (camera::Camera::instance() == nullptr)
@ -1148,9 +1124,9 @@ bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertiseme
manufacturer_data.legacy_data.assign(manufacturer_data.data.begin(), manufacturer_data.data.end()); manufacturer_data.legacy_data.assign(manufacturer_data.data.begin(), manufacturer_data.data.end());
manufacturer_data.data.clear(); manufacturer_data.data.clear();
} }
return this->send_message(resp); return this->send_message(resp, BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
} }
return this->send_message(msg); return this->send_message(msg, BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
} }
void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg);
@ -1284,8 +1260,8 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A
auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity); auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity);
AlarmControlPanelStateResponse resp; AlarmControlPanelStateResponse resp;
resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state()); resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state());
fill_entity_state_base(a_alarm_control_panel, resp); return fill_and_encode_entity_state(a_alarm_control_panel, resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn,
return encode_message_to_buffer(resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); remaining_size, is_single);
} }
uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn,
uint32_t remaining_size, bool is_single) { uint32_t remaining_size, bool is_single) {
@ -1294,10 +1270,8 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP
msg.supported_features = a_alarm_control_panel->get_supported_features(); msg.supported_features = a_alarm_control_panel->get_supported_features();
msg.requires_code = a_alarm_control_panel->get_requires_code(); msg.requires_code = a_alarm_control_panel->get_requires_code();
msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm();
msg.unique_id = get_default_unique_id("alarm_control_panel", a_alarm_control_panel); return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE,
fill_entity_info_base(a_alarm_control_panel, msg); conn, remaining_size, is_single);
return encode_message_to_buffer(msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE, conn, remaining_size,
is_single);
} }
void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
@ -1338,8 +1312,7 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, const std::
uint32_t remaining_size, bool is_single) { uint32_t remaining_size, bool is_single) {
EventResponse resp; EventResponse resp;
resp.event_type = event_type; resp.event_type = event_type;
fill_entity_state_base(event, resp); return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -1349,9 +1322,8 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
msg.device_class = event->get_device_class(); msg.device_class = event->get_device_class();
for (const auto &event_type : event->get_event_types()) for (const auto &event_type : event->get_event_types())
msg.event_types.push_back(event_type); msg.event_types.push_back(event_type);
msg.unique_id = get_default_unique_id("event", event); return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(event, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
#endif #endif
@ -1377,17 +1349,15 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection
resp.release_summary = update->update_info.summary; resp.release_summary = update->update_info.summary;
resp.release_url = update->update_info.release_url; resp.release_url = update->update_info.release_url;
} }
fill_entity_state_base(update, resp); return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
return encode_message_to_buffer(resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
auto *update = static_cast<update::UpdateEntity *>(entity); auto *update = static_cast<update::UpdateEntity *>(entity);
ListEntitiesUpdateResponse msg; ListEntitiesUpdateResponse msg;
msg.device_class = update->get_device_class(); msg.device_class = update->get_device_class();
msg.unique_id = get_default_unique_id("update", update); return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size,
fill_entity_info_base(update, msg); is_single);
return encode_message_to_buffer(msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::update_command(const UpdateCommandRequest &msg) { void APIConnection::update_command(const UpdateCommandRequest &msg) {
ENTITY_COMMAND_GET(update::UpdateEntity, update, update) ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
@ -1410,9 +1380,6 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) {
#endif #endif
bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) { bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) {
if (this->flags_.log_subscription < level)
return false;
// Pre-calculate message size to avoid reallocations // Pre-calculate message size to avoid reallocations
uint32_t msg_size = 0; uint32_t msg_size = 0;
@ -1435,6 +1402,24 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
} }
void APIConnection::complete_authentication_() {
// Early return if already authenticated
if (this->flags_.connection_state == static_cast<uint8_t>(ConnectionState::AUTHENTICATED)) {
return;
}
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
#endif
#ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) {
this->send_time_request();
}
#endif
}
HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse APIConnection::hello(const HelloRequest &msg) {
this->client_info_ = msg.client_info; this->client_info_ = msg.client_info;
this->client_peername_ = this->helper_->getpeername(); this->client_peername_ = this->helper_->getpeername();
@ -1450,7 +1435,14 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) {
resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
resp.name = App.get_name(); resp.name = App.get_name();
#ifdef USE_API_PASSWORD
// Password required - wait for authentication
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::CONNECTED); this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::CONNECTED);
#else
// No password configured - auto-authenticate
this->complete_authentication_();
#endif
return resp; return resp;
} }
ConnectResponse APIConnection::connect(const ConnectRequest &msg) { ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
@ -1463,29 +1455,22 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
// bool invalid_password = 1; // bool invalid_password = 1;
resp.invalid_password = !correct; resp.invalid_password = !correct;
if (correct) { if (correct) {
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); this->complete_authentication_();
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
#endif
#ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) {
this->send_time_request();
}
#endif
} }
return resp; return resp;
} }
DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
DeviceInfoResponse resp{}; DeviceInfoResponse resp{};
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
resp.uses_password = this->parent_->uses_password(); resp.uses_password = true;
#else #else
resp.uses_password = false; resp.uses_password = false;
#endif #endif
resp.name = App.get_name(); resp.name = App.get_name();
resp.friendly_name = App.get_friendly_name(); resp.friendly_name = App.get_friendly_name();
#ifdef USE_AREAS
resp.suggested_area = App.get_area(); resp.suggested_area = App.get_area();
#endif
resp.mac_address = get_mac_address_pretty(); resp.mac_address = get_mac_address_pretty();
resp.esphome_version = ESPHOME_VERSION; resp.esphome_version = ESPHOME_VERSION;
resp.compilation_time = App.get_compilation_time(); resp.compilation_time = App.get_compilation_time();
@ -1596,7 +1581,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno); api_error_to_str(err), errno);
return false; return false;
} }
@ -1617,12 +1602,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
return false; return false;
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(),
ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); api_error_to_str(err), errno);
} else {
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno);
}
return false; return false;
} }
// Do not set last_traffic_ on send // Do not set last_traffic_ on send
@ -1630,11 +1611,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
} }
void APIConnection::on_unauthenticated_access() { void APIConnection::on_unauthenticated_access() {
this->on_fatal_error(); this->on_fatal_error();
ESP_LOGD(TAG, "%s requested access without authentication", this->get_client_combined_info().c_str()); ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str());
} }
void APIConnection::on_no_setup_connection() { void APIConnection::on_no_setup_connection() {
this->on_fatal_error(); this->on_fatal_error();
ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str()); ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str());
} }
void APIConnection::on_fatal_error() { void APIConnection::on_fatal_error() {
this->helper_->close(); this->helper_->close();
@ -1662,8 +1643,15 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type,
uint8_t estimated_size) { uint8_t estimated_size) {
// Insert at front for high priority messages (no deduplication check) // Add high priority message and swap to front
items.insert(items.begin(), BatchItem(entity, std::move(creator), message_type, estimated_size)); // This avoids expensive vector::insert which shifts all elements
// Note: We only ever have one high-priority message at a time (ping OR disconnect)
// If we're disconnecting, pings are blocked, so this simple swap is sufficient
items.emplace_back(entity, std::move(creator), message_type, estimated_size);
if (items.size() > 1) {
// Swap the new high-priority item to the front
std::swap(items.front(), items.back());
}
} }
bool APIConnection::schedule_batch_() { bool APIConnection::schedule_batch_() {
@ -1799,12 +1787,8 @@ void APIConnection::process_batch_() {
this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info); this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info);
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error(); on_fatal_error();
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
ESP_LOGW(TAG, "%s: Connection reset during batch write", this->get_client_combined_info().c_str()); errno);
} else {
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno);
}
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP

View File

@ -111,7 +111,7 @@ class APIConnection : public APIServerConnection {
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
if (!this->flags_.service_call_subscription) if (!this->flags_.service_call_subscription)
return; return;
this->send_message(call); this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
} }
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
@ -133,7 +133,7 @@ class APIConnection : public APIServerConnection {
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void send_time_request() { void send_time_request() {
GetTimeRequest req; GetTimeRequest req;
this->send_message(req); this->send_message(req, GetTimeRequest::MESSAGE_TYPE);
} }
#endif #endif
@ -209,6 +209,7 @@ class APIConnection : public APIServerConnection {
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED || return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED ||
this->is_authenticated(); this->is_authenticated();
} }
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
void on_fatal_error() override; void on_fatal_error() override;
void on_unauthenticated_access() override; void on_unauthenticated_access() override;
void on_no_setup_connection() override; void on_no_setup_connection() override;
@ -273,36 +274,43 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
protected: protected:
// Helper function to fill common entity info fields // Helper function to handle authentication completion
static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) { void complete_authentication_();
// Set common fields that are shared by all entity types
response.key = entity->get_object_id_hash();
response.object_id = entity->get_object_id();
if (entity->has_own_name())
response.name = entity->get_name();
// Set common EntityBase properties
response.icon = entity->get_icon();
response.disabled_by_default = entity->is_disabled_by_default();
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
#ifdef USE_DEVICES
response.device_id = entity->get_device_id();
#endif
}
// Helper function to fill common entity state fields
static void fill_entity_state_base(esphome::EntityBase *entity, StateResponseProtoMessage &response) {
response.key = entity->get_object_id_hash();
#ifdef USE_DEVICES
response.device_id = entity->get_device_id();
#endif
}
// Non-template helper to encode any ProtoMessage // Non-template helper to encode any ProtoMessage
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single); uint32_t remaining_size, bool is_single);
// Helper to fill entity state base and encode message
static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type,
APIConnection *conn, uint32_t remaining_size, bool is_single) {
msg.key = entity->get_object_id_hash();
#ifdef USE_DEVICES
msg.device_id = entity->get_device_id();
#endif
return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single);
}
// Helper to fill entity info base and encode message
static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type,
APIConnection *conn, uint32_t remaining_size, bool is_single) {
// Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash();
msg.object_id = entity->get_object_id();
if (entity->has_own_name())
msg.name = entity->get_name();
// Set common EntityBase properties
msg.icon = entity->get_icon();
msg.disabled_by_default = entity->is_disabled_by_default();
msg.entity_category = static_cast<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);
}
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
// Helper to check voice assistant validity and connection ownership // Helper to check voice assistant validity and connection ownership
inline bool check_voice_assistant_api_connection_() const; inline bool check_voice_assistant_api_connection_() const;

View File

@ -23,3 +23,8 @@ extend google.protobuf.MessageOptions {
optional bool no_delay = 1040 [default=false]; optional bool no_delay = 1040 [default=false];
optional string base_class = 1041; 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

View File

@ -291,7 +291,6 @@ class InfoResponseProtoMessage : public ProtoMessage {
std::string object_id{}; std::string object_id{};
uint32_t key{0}; uint32_t key{0};
std::string name{}; std::string name{};
std::string unique_id{};
bool disabled_by_default{false}; bool disabled_by_default{false};
std::string icon{}; std::string icon{};
enums::EntityCategory entity_category{}; enums::EntityCategory entity_category{};
@ -309,7 +308,7 @@ class StateResponseProtoMessage : public ProtoMessage {
protected: protected:
}; };
class CommandProtoMessage : public ProtoMessage { class CommandProtoMessage : public ProtoDecodableMessage {
public: public:
~CommandProtoMessage() override = default; ~CommandProtoMessage() override = default;
uint32_t key{0}; uint32_t key{0};
@ -317,7 +316,7 @@ class CommandProtoMessage : public ProtoMessage {
protected: protected:
}; };
class HelloRequest : public ProtoMessage { class HelloRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 1; static constexpr uint8_t MESSAGE_TYPE = 1;
static constexpr uint8_t ESTIMATED_SIZE = 17; static constexpr uint8_t ESTIMATED_SIZE = 17;
@ -354,7 +353,7 @@ class HelloResponse : public ProtoMessage {
protected: protected:
}; };
class ConnectRequest : public ProtoMessage { class ConnectRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 3; static constexpr uint8_t MESSAGE_TYPE = 3;
static constexpr uint8_t ESTIMATED_SIZE = 9; static constexpr uint8_t ESTIMATED_SIZE = 9;
@ -385,7 +384,7 @@ class ConnectResponse : public ProtoMessage {
protected: protected:
}; };
class DisconnectRequest : public ProtoMessage { class DisconnectRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 5; static constexpr uint8_t MESSAGE_TYPE = 5;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -398,7 +397,7 @@ class DisconnectRequest : public ProtoMessage {
protected: protected:
}; };
class DisconnectResponse : public ProtoMessage { class DisconnectResponse : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 6; static constexpr uint8_t MESSAGE_TYPE = 6;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -411,7 +410,7 @@ class DisconnectResponse : public ProtoMessage {
protected: protected:
}; };
class PingRequest : public ProtoMessage { class PingRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 7; static constexpr uint8_t MESSAGE_TYPE = 7;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -424,7 +423,7 @@ class PingRequest : public ProtoMessage {
protected: protected:
}; };
class PingResponse : public ProtoMessage { class PingResponse : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 8; static constexpr uint8_t MESSAGE_TYPE = 8;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -437,7 +436,7 @@ class PingResponse : public ProtoMessage {
protected: protected:
}; };
class DeviceInfoRequest : public ProtoMessage { class DeviceInfoRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 9; static constexpr uint8_t MESSAGE_TYPE = 9;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -461,8 +460,6 @@ class AreaInfo : public ProtoMessage {
#endif #endif
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class DeviceInfo : public ProtoMessage { class DeviceInfo : public ProtoMessage {
public: public:
@ -476,8 +473,6 @@ class DeviceInfo : public ProtoMessage {
#endif #endif
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class DeviceInfoResponse : public ProtoMessage { class DeviceInfoResponse : public ProtoMessage {
public: public:
@ -492,22 +487,50 @@ class DeviceInfoResponse : public ProtoMessage {
std::string esphome_version{}; std::string esphome_version{};
std::string compilation_time{}; std::string compilation_time{};
std::string model{}; std::string model{};
#ifdef USE_DEEP_SLEEP
bool has_deep_sleep{false}; bool has_deep_sleep{false};
#endif
#ifdef ESPHOME_PROJECT_NAME
std::string project_name{}; std::string project_name{};
#endif
#ifdef ESPHOME_PROJECT_NAME
std::string project_version{}; std::string project_version{};
#endif
#ifdef USE_WEBSERVER
uint32_t webserver_port{0}; uint32_t webserver_port{0};
#endif
#ifdef USE_BLUETOOTH_PROXY
uint32_t legacy_bluetooth_proxy_version{0}; uint32_t legacy_bluetooth_proxy_version{0};
#endif
#ifdef USE_BLUETOOTH_PROXY
uint32_t bluetooth_proxy_feature_flags{0}; uint32_t bluetooth_proxy_feature_flags{0};
#endif
std::string manufacturer{}; std::string manufacturer{};
std::string friendly_name{}; std::string friendly_name{};
#ifdef USE_VOICE_ASSISTANT
uint32_t legacy_voice_assistant_version{0}; uint32_t legacy_voice_assistant_version{0};
#endif
#ifdef USE_VOICE_ASSISTANT
uint32_t voice_assistant_feature_flags{0}; uint32_t voice_assistant_feature_flags{0};
#endif
#ifdef USE_AREAS
std::string suggested_area{}; std::string suggested_area{};
#endif
#ifdef USE_BLUETOOTH_PROXY
std::string bluetooth_mac_address{}; std::string bluetooth_mac_address{};
#endif
#ifdef USE_API_NOISE
bool api_encryption_supported{false}; bool api_encryption_supported{false};
#endif
#ifdef USE_DEVICES
std::vector<DeviceInfo> devices{}; std::vector<DeviceInfo> devices{};
#endif
#ifdef USE_AREAS
std::vector<AreaInfo> areas{}; std::vector<AreaInfo> areas{};
#endif
#ifdef USE_AREAS
AreaInfo area{}; AreaInfo area{};
#endif
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -516,7 +539,7 @@ class DeviceInfoResponse : public ProtoMessage {
protected: protected:
}; };
class ListEntitiesRequest : public ProtoMessage { class ListEntitiesRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 11; static constexpr uint8_t MESSAGE_TYPE = 11;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -542,7 +565,7 @@ class ListEntitiesDoneResponse : public ProtoMessage {
protected: protected:
}; };
class SubscribeStatesRequest : public ProtoMessage { class SubscribeStatesRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 20; static constexpr uint8_t MESSAGE_TYPE = 20;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -559,7 +582,7 @@ class SubscribeStatesRequest : public ProtoMessage {
class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 12; static constexpr uint8_t MESSAGE_TYPE = 12;
static constexpr uint8_t ESTIMATED_SIZE = 60; static constexpr uint8_t ESTIMATED_SIZE = 51;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_binary_sensor_response"; } const char *message_name() const override { return "list_entities_binary_sensor_response"; }
#endif #endif
@ -595,7 +618,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage {
class ListEntitiesCoverResponse : public InfoResponseProtoMessage { class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 13; static constexpr uint8_t MESSAGE_TYPE = 13;
static constexpr uint8_t ESTIMATED_SIZE = 66; static constexpr uint8_t ESTIMATED_SIZE = 57;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_cover_response"; } const char *message_name() const override { return "list_entities_cover_response"; }
#endif #endif
@ -658,7 +681,7 @@ class CoverCommandRequest : public CommandProtoMessage {
class ListEntitiesFanResponse : public InfoResponseProtoMessage { class ListEntitiesFanResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 14; static constexpr uint8_t MESSAGE_TYPE = 14;
static constexpr uint8_t ESTIMATED_SIZE = 77; static constexpr uint8_t ESTIMATED_SIZE = 68;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_fan_response"; } const char *message_name() const override { return "list_entities_fan_response"; }
#endif #endif
@ -729,7 +752,7 @@ class FanCommandRequest : public CommandProtoMessage {
class ListEntitiesLightResponse : public InfoResponseProtoMessage { class ListEntitiesLightResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 15; static constexpr uint8_t MESSAGE_TYPE = 15;
static constexpr uint8_t ESTIMATED_SIZE = 90; static constexpr uint8_t ESTIMATED_SIZE = 81;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_light_response"; } const char *message_name() const override { return "list_entities_light_response"; }
#endif #endif
@ -823,7 +846,7 @@ class LightCommandRequest : public CommandProtoMessage {
class ListEntitiesSensorResponse : public InfoResponseProtoMessage { class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 16; static constexpr uint8_t MESSAGE_TYPE = 16;
static constexpr uint8_t ESTIMATED_SIZE = 77; static constexpr uint8_t ESTIMATED_SIZE = 68;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_sensor_response"; } const char *message_name() const override { return "list_entities_sensor_response"; }
#endif #endif
@ -863,7 +886,7 @@ class SensorStateResponse : public StateResponseProtoMessage {
class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { class ListEntitiesSwitchResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 17; static constexpr uint8_t MESSAGE_TYPE = 17;
static constexpr uint8_t ESTIMATED_SIZE = 60; static constexpr uint8_t ESTIMATED_SIZE = 51;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_switch_response"; } const char *message_name() const override { return "list_entities_switch_response"; }
#endif #endif
@ -914,7 +937,7 @@ class SwitchCommandRequest : public CommandProtoMessage {
class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 18; static constexpr uint8_t MESSAGE_TYPE = 18;
static constexpr uint8_t ESTIMATED_SIZE = 58; static constexpr uint8_t ESTIMATED_SIZE = 49;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_text_sensor_response"; } const char *message_name() const override { return "list_entities_text_sensor_response"; }
#endif #endif
@ -945,7 +968,7 @@ class TextSensorStateResponse : public StateResponseProtoMessage {
protected: protected:
}; };
#endif #endif
class SubscribeLogsRequest : public ProtoMessage { class SubscribeLogsRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 28; static constexpr uint8_t MESSAGE_TYPE = 28;
static constexpr uint8_t ESTIMATED_SIZE = 4; static constexpr uint8_t ESTIMATED_SIZE = 4;
@ -980,7 +1003,7 @@ class SubscribeLogsResponse : public ProtoMessage {
protected: protected:
}; };
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
class NoiseEncryptionSetKeyRequest : public ProtoMessage { class NoiseEncryptionSetKeyRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 124; static constexpr uint8_t MESSAGE_TYPE = 124;
static constexpr uint8_t ESTIMATED_SIZE = 9; static constexpr uint8_t ESTIMATED_SIZE = 9;
@ -1012,7 +1035,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage {
protected: protected:
}; };
#endif #endif
class SubscribeHomeassistantServicesRequest : public ProtoMessage { class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 34; static constexpr uint8_t MESSAGE_TYPE = 34;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -1036,7 +1059,6 @@ class HomeassistantServiceMap : public ProtoMessage {
#endif #endif
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
}; };
class HomeassistantServiceResponse : public ProtoMessage { class HomeassistantServiceResponse : public ProtoMessage {
public: public:
@ -1058,7 +1080,7 @@ class HomeassistantServiceResponse : public ProtoMessage {
protected: protected:
}; };
class SubscribeHomeAssistantStatesRequest : public ProtoMessage { class SubscribeHomeAssistantStatesRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 38; static constexpr uint8_t MESSAGE_TYPE = 38;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -1089,7 +1111,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage {
protected: protected:
}; };
class HomeAssistantStateResponse : public ProtoMessage { class HomeAssistantStateResponse : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 40; static constexpr uint8_t MESSAGE_TYPE = 40;
static constexpr uint8_t ESTIMATED_SIZE = 27; static constexpr uint8_t ESTIMATED_SIZE = 27;
@ -1106,7 +1128,7 @@ class HomeAssistantStateResponse : public ProtoMessage {
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
}; };
class GetTimeRequest : public ProtoMessage { class GetTimeRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 36; static constexpr uint8_t MESSAGE_TYPE = 36;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -1119,7 +1141,7 @@ class GetTimeRequest : public ProtoMessage {
protected: protected:
}; };
class GetTimeResponse : public ProtoMessage { class GetTimeResponse : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 37; static constexpr uint8_t MESSAGE_TYPE = 37;
static constexpr uint8_t ESTIMATED_SIZE = 5; static constexpr uint8_t ESTIMATED_SIZE = 5;
@ -1148,8 +1170,6 @@ class ListEntitiesServicesArgument : public ProtoMessage {
#endif #endif
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesServicesResponse : public ProtoMessage { class ListEntitiesServicesResponse : public ProtoMessage {
public: public:
@ -1169,7 +1189,7 @@ class ListEntitiesServicesResponse : public ProtoMessage {
protected: protected:
}; };
class ExecuteServiceArgument : public ProtoMessage { class ExecuteServiceArgument : public ProtoDecodableMessage {
public: public:
bool bool_{false}; bool bool_{false};
int32_t legacy_int{0}; int32_t legacy_int{0};
@ -1180,8 +1200,6 @@ class ExecuteServiceArgument : public ProtoMessage {
std::vector<int32_t> int_array{}; std::vector<int32_t> int_array{};
std::vector<float> float_array{}; std::vector<float> float_array{};
std::vector<std::string> string_array{}; std::vector<std::string> string_array{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@ -1191,7 +1209,7 @@ class ExecuteServiceArgument : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ExecuteServiceRequest : public ProtoMessage { class ExecuteServiceRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 42; static constexpr uint8_t MESSAGE_TYPE = 42;
static constexpr uint8_t ESTIMATED_SIZE = 39; static constexpr uint8_t ESTIMATED_SIZE = 39;
@ -1213,7 +1231,7 @@ class ExecuteServiceRequest : public ProtoMessage {
class ListEntitiesCameraResponse : public InfoResponseProtoMessage { class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 43; static constexpr uint8_t MESSAGE_TYPE = 43;
static constexpr uint8_t ESTIMATED_SIZE = 49; static constexpr uint8_t ESTIMATED_SIZE = 40;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_camera_response"; } const char *message_name() const override { return "list_entities_camera_response"; }
#endif #endif
@ -1242,7 +1260,7 @@ class CameraImageResponse : public StateResponseProtoMessage {
protected: protected:
}; };
class CameraImageRequest : public ProtoMessage { class CameraImageRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 45; static constexpr uint8_t MESSAGE_TYPE = 45;
static constexpr uint8_t ESTIMATED_SIZE = 4; static constexpr uint8_t ESTIMATED_SIZE = 4;
@ -1263,7 +1281,7 @@ class CameraImageRequest : public ProtoMessage {
class ListEntitiesClimateResponse : public InfoResponseProtoMessage { class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 46; static constexpr uint8_t MESSAGE_TYPE = 46;
static constexpr uint8_t ESTIMATED_SIZE = 156; static constexpr uint8_t ESTIMATED_SIZE = 147;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_climate_response"; } const char *message_name() const override { return "list_entities_climate_response"; }
#endif #endif
@ -1365,7 +1383,7 @@ class ClimateCommandRequest : public CommandProtoMessage {
class ListEntitiesNumberResponse : public InfoResponseProtoMessage { class ListEntitiesNumberResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 49; static constexpr uint8_t MESSAGE_TYPE = 49;
static constexpr uint8_t ESTIMATED_SIZE = 84; static constexpr uint8_t ESTIMATED_SIZE = 75;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_number_response"; } const char *message_name() const override { return "list_entities_number_response"; }
#endif #endif
@ -1421,7 +1439,7 @@ class NumberCommandRequest : public CommandProtoMessage {
class ListEntitiesSelectResponse : public InfoResponseProtoMessage { class ListEntitiesSelectResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 52; static constexpr uint8_t MESSAGE_TYPE = 52;
static constexpr uint8_t ESTIMATED_SIZE = 67; static constexpr uint8_t ESTIMATED_SIZE = 58;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_select_response"; } const char *message_name() const override { return "list_entities_select_response"; }
#endif #endif
@ -1473,7 +1491,7 @@ class SelectCommandRequest : public CommandProtoMessage {
class ListEntitiesSirenResponse : public InfoResponseProtoMessage { class ListEntitiesSirenResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 55; static constexpr uint8_t MESSAGE_TYPE = 55;
static constexpr uint8_t ESTIMATED_SIZE = 71; static constexpr uint8_t ESTIMATED_SIZE = 62;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_siren_response"; } const char *message_name() const override { return "list_entities_siren_response"; }
#endif #endif
@ -1533,7 +1551,7 @@ class SirenCommandRequest : public CommandProtoMessage {
class ListEntitiesLockResponse : public InfoResponseProtoMessage { class ListEntitiesLockResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 58; static constexpr uint8_t MESSAGE_TYPE = 58;
static constexpr uint8_t ESTIMATED_SIZE = 64; static constexpr uint8_t ESTIMATED_SIZE = 55;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_lock_response"; } const char *message_name() const override { return "list_entities_lock_response"; }
#endif #endif
@ -1589,7 +1607,7 @@ class LockCommandRequest : public CommandProtoMessage {
class ListEntitiesButtonResponse : public InfoResponseProtoMessage { class ListEntitiesButtonResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 61; static constexpr uint8_t MESSAGE_TYPE = 61;
static constexpr uint8_t ESTIMATED_SIZE = 58; static constexpr uint8_t ESTIMATED_SIZE = 49;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_button_response"; } const char *message_name() const override { return "list_entities_button_response"; }
#endif #endif
@ -1633,13 +1651,11 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
#endif #endif
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 63; static constexpr uint8_t MESSAGE_TYPE = 63;
static constexpr uint8_t ESTIMATED_SIZE = 85; static constexpr uint8_t ESTIMATED_SIZE = 76;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_media_player_response"; } const char *message_name() const override { return "list_entities_media_player_response"; }
#endif #endif
@ -1697,7 +1713,7 @@ class MediaPlayerCommandRequest : public CommandProtoMessage {
}; };
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 66; static constexpr uint8_t MESSAGE_TYPE = 66;
static constexpr uint8_t ESTIMATED_SIZE = 4; static constexpr uint8_t ESTIMATED_SIZE = 4;
@ -1724,8 +1740,6 @@ class BluetoothServiceData : public ProtoMessage {
#endif #endif
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class BluetoothLEAdvertisementResponse : public ProtoMessage { class BluetoothLEAdvertisementResponse : public ProtoMessage {
public: public:
@ -1754,7 +1768,8 @@ class BluetoothLERawAdvertisement : public ProtoMessage {
uint64_t address{0}; uint64_t address{0};
int32_t rssi{0}; int32_t rssi{0};
uint32_t address_type{0}; uint32_t address_type{0};
std::string data{}; uint8_t data[62]{};
uint8_t data_len{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -1762,8 +1777,6 @@ class BluetoothLERawAdvertisement : public ProtoMessage {
#endif #endif
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class BluetoothLERawAdvertisementsResponse : public ProtoMessage { class BluetoothLERawAdvertisementsResponse : public ProtoMessage {
public: public:
@ -1781,7 +1794,7 @@ class BluetoothLERawAdvertisementsResponse : public ProtoMessage {
protected: protected:
}; };
class BluetoothDeviceRequest : public ProtoMessage { class BluetoothDeviceRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 68; static constexpr uint8_t MESSAGE_TYPE = 68;
static constexpr uint8_t ESTIMATED_SIZE = 12; static constexpr uint8_t ESTIMATED_SIZE = 12;
@ -1818,7 +1831,7 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage {
protected: protected:
}; };
class BluetoothGATTGetServicesRequest : public ProtoMessage { class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 70; static constexpr uint8_t MESSAGE_TYPE = 70;
static constexpr uint8_t ESTIMATED_SIZE = 4; static constexpr uint8_t ESTIMATED_SIZE = 4;
@ -1844,7 +1857,6 @@ class BluetoothGATTDescriptor : public ProtoMessage {
#endif #endif
protected: protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class BluetoothGATTCharacteristic : public ProtoMessage { class BluetoothGATTCharacteristic : public ProtoMessage {
public: public:
@ -1859,8 +1871,6 @@ class BluetoothGATTCharacteristic : public ProtoMessage {
#endif #endif
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class BluetoothGATTService : public ProtoMessage { class BluetoothGATTService : public ProtoMessage {
public: public:
@ -1874,8 +1884,6 @@ class BluetoothGATTService : public ProtoMessage {
#endif #endif
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class BluetoothGATTGetServicesResponse : public ProtoMessage { class BluetoothGATTGetServicesResponse : public ProtoMessage {
public: public:
@ -1910,7 +1918,7 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage {
protected: protected:
}; };
class BluetoothGATTReadRequest : public ProtoMessage { class BluetoothGATTReadRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 73; static constexpr uint8_t MESSAGE_TYPE = 73;
static constexpr uint8_t ESTIMATED_SIZE = 8; static constexpr uint8_t ESTIMATED_SIZE = 8;
@ -1944,7 +1952,7 @@ class BluetoothGATTReadResponse : public ProtoMessage {
protected: protected:
}; };
class BluetoothGATTWriteRequest : public ProtoMessage { class BluetoothGATTWriteRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 75; static constexpr uint8_t MESSAGE_TYPE = 75;
static constexpr uint8_t ESTIMATED_SIZE = 19; static constexpr uint8_t ESTIMATED_SIZE = 19;
@ -1963,7 +1971,7 @@ class BluetoothGATTWriteRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class BluetoothGATTReadDescriptorRequest : public ProtoMessage { class BluetoothGATTReadDescriptorRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 76; static constexpr uint8_t MESSAGE_TYPE = 76;
static constexpr uint8_t ESTIMATED_SIZE = 8; static constexpr uint8_t ESTIMATED_SIZE = 8;
@ -1979,7 +1987,7 @@ class BluetoothGATTReadDescriptorRequest : public ProtoMessage {
protected: protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class BluetoothGATTWriteDescriptorRequest : public ProtoMessage { class BluetoothGATTWriteDescriptorRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 77; static constexpr uint8_t MESSAGE_TYPE = 77;
static constexpr uint8_t ESTIMATED_SIZE = 17; static constexpr uint8_t ESTIMATED_SIZE = 17;
@ -1997,7 +2005,7 @@ class BluetoothGATTWriteDescriptorRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class BluetoothGATTNotifyRequest : public ProtoMessage { class BluetoothGATTNotifyRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 78; static constexpr uint8_t MESSAGE_TYPE = 78;
static constexpr uint8_t ESTIMATED_SIZE = 10; static constexpr uint8_t ESTIMATED_SIZE = 10;
@ -2032,7 +2040,7 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage {
protected: protected:
}; };
class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { class SubscribeBluetoothConnectionsFreeRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 80; static constexpr uint8_t MESSAGE_TYPE = 80;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -2151,7 +2159,7 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage {
protected: protected:
}; };
class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 87; static constexpr uint8_t MESSAGE_TYPE = 87;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -2199,7 +2207,7 @@ class BluetoothScannerStateResponse : public ProtoMessage {
protected: protected:
}; };
class BluetoothScannerSetModeRequest : public ProtoMessage { class BluetoothScannerSetModeRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 127; static constexpr uint8_t MESSAGE_TYPE = 127;
static constexpr uint8_t ESTIMATED_SIZE = 2; static constexpr uint8_t ESTIMATED_SIZE = 2;
@ -2216,7 +2224,7 @@ class BluetoothScannerSetModeRequest : public ProtoMessage {
}; };
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
class SubscribeVoiceAssistantRequest : public ProtoMessage { class SubscribeVoiceAssistantRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 89; static constexpr uint8_t MESSAGE_TYPE = 89;
static constexpr uint8_t ESTIMATED_SIZE = 6; static constexpr uint8_t ESTIMATED_SIZE = 6;
@ -2244,8 +2252,6 @@ class VoiceAssistantAudioSettings : public ProtoMessage {
#endif #endif
protected: protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class VoiceAssistantRequest : public ProtoMessage { class VoiceAssistantRequest : public ProtoMessage {
public: public:
@ -2267,7 +2273,7 @@ class VoiceAssistantRequest : public ProtoMessage {
protected: protected:
}; };
class VoiceAssistantResponse : public ProtoMessage { class VoiceAssistantResponse : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 91; static constexpr uint8_t MESSAGE_TYPE = 91;
static constexpr uint8_t ESTIMATED_SIZE = 6; static constexpr uint8_t ESTIMATED_SIZE = 6;
@ -2283,12 +2289,10 @@ class VoiceAssistantResponse : public ProtoMessage {
protected: protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class VoiceAssistantEventData : public ProtoMessage { class VoiceAssistantEventData : public ProtoDecodableMessage {
public: public:
std::string name{}; std::string name{};
std::string value{}; std::string value{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@ -2296,7 +2300,7 @@ class VoiceAssistantEventData : public ProtoMessage {
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
}; };
class VoiceAssistantEventResponse : public ProtoMessage { class VoiceAssistantEventResponse : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 92; static constexpr uint8_t MESSAGE_TYPE = 92;
static constexpr uint8_t ESTIMATED_SIZE = 36; static constexpr uint8_t ESTIMATED_SIZE = 36;
@ -2313,7 +2317,7 @@ class VoiceAssistantEventResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class VoiceAssistantAudio : public ProtoMessage { class VoiceAssistantAudio : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 106; static constexpr uint8_t MESSAGE_TYPE = 106;
static constexpr uint8_t ESTIMATED_SIZE = 11; static constexpr uint8_t ESTIMATED_SIZE = 11;
@ -2332,7 +2336,7 @@ class VoiceAssistantAudio : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class VoiceAssistantTimerEventResponse : public ProtoMessage { class VoiceAssistantTimerEventResponse : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 115; static constexpr uint8_t MESSAGE_TYPE = 115;
static constexpr uint8_t ESTIMATED_SIZE = 30; static constexpr uint8_t ESTIMATED_SIZE = 30;
@ -2353,7 +2357,7 @@ class VoiceAssistantTimerEventResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class VoiceAssistantAnnounceRequest : public ProtoMessage { class VoiceAssistantAnnounceRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 119; static constexpr uint8_t MESSAGE_TYPE = 119;
static constexpr uint8_t ESTIMATED_SIZE = 29; static constexpr uint8_t ESTIMATED_SIZE = 29;
@ -2400,9 +2404,8 @@ class VoiceAssistantWakeWord : public ProtoMessage {
#endif #endif
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
}; };
class VoiceAssistantConfigurationRequest : public ProtoMessage { class VoiceAssistantConfigurationRequest : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 121; static constexpr uint8_t MESSAGE_TYPE = 121;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@ -2433,7 +2436,7 @@ class VoiceAssistantConfigurationResponse : public ProtoMessage {
protected: protected:
}; };
class VoiceAssistantSetConfiguration : public ProtoMessage { class VoiceAssistantSetConfiguration : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 123; static constexpr uint8_t MESSAGE_TYPE = 123;
static constexpr uint8_t ESTIMATED_SIZE = 18; static constexpr uint8_t ESTIMATED_SIZE = 18;
@ -2453,7 +2456,7 @@ class VoiceAssistantSetConfiguration : public ProtoMessage {
class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 94; static constexpr uint8_t MESSAGE_TYPE = 94;
static constexpr uint8_t ESTIMATED_SIZE = 57; static constexpr uint8_t ESTIMATED_SIZE = 48;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_alarm_control_panel_response"; } const char *message_name() const override { return "list_entities_alarm_control_panel_response"; }
#endif #endif
@ -2507,7 +2510,7 @@ class AlarmControlPanelCommandRequest : public CommandProtoMessage {
class ListEntitiesTextResponse : public InfoResponseProtoMessage { class ListEntitiesTextResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 97; static constexpr uint8_t MESSAGE_TYPE = 97;
static constexpr uint8_t ESTIMATED_SIZE = 68; static constexpr uint8_t ESTIMATED_SIZE = 59;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_text_response"; } const char *message_name() const override { return "list_entities_text_response"; }
#endif #endif
@ -2562,7 +2565,7 @@ class TextCommandRequest : public CommandProtoMessage {
class ListEntitiesDateResponse : public InfoResponseProtoMessage { class ListEntitiesDateResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 100; static constexpr uint8_t MESSAGE_TYPE = 100;
static constexpr uint8_t ESTIMATED_SIZE = 49; static constexpr uint8_t ESTIMATED_SIZE = 40;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_date_response"; } const char *message_name() const override { return "list_entities_date_response"; }
#endif #endif
@ -2616,7 +2619,7 @@ class DateCommandRequest : public CommandProtoMessage {
class ListEntitiesTimeResponse : public InfoResponseProtoMessage { class ListEntitiesTimeResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 103; static constexpr uint8_t MESSAGE_TYPE = 103;
static constexpr uint8_t ESTIMATED_SIZE = 49; static constexpr uint8_t ESTIMATED_SIZE = 40;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_time_response"; } const char *message_name() const override { return "list_entities_time_response"; }
#endif #endif
@ -2670,7 +2673,7 @@ class TimeCommandRequest : public CommandProtoMessage {
class ListEntitiesEventResponse : public InfoResponseProtoMessage { class ListEntitiesEventResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 107; static constexpr uint8_t MESSAGE_TYPE = 107;
static constexpr uint8_t ESTIMATED_SIZE = 76; static constexpr uint8_t ESTIMATED_SIZE = 67;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_event_response"; } const char *message_name() const override { return "list_entities_event_response"; }
#endif #endif
@ -2705,7 +2708,7 @@ class EventResponse : public StateResponseProtoMessage {
class ListEntitiesValveResponse : public InfoResponseProtoMessage { class ListEntitiesValveResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 109; static constexpr uint8_t MESSAGE_TYPE = 109;
static constexpr uint8_t ESTIMATED_SIZE = 64; static constexpr uint8_t ESTIMATED_SIZE = 55;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_valve_response"; } const char *message_name() const override { return "list_entities_valve_response"; }
#endif #endif
@ -2761,7 +2764,7 @@ class ValveCommandRequest : public CommandProtoMessage {
class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 112; static constexpr uint8_t MESSAGE_TYPE = 112;
static constexpr uint8_t ESTIMATED_SIZE = 49; static constexpr uint8_t ESTIMATED_SIZE = 40;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_date_time_response"; } const char *message_name() const override { return "list_entities_date_time_response"; }
#endif #endif
@ -2811,7 +2814,7 @@ class DateTimeCommandRequest : public CommandProtoMessage {
class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { class ListEntitiesUpdateResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 116; static constexpr uint8_t MESSAGE_TYPE = 116;
static constexpr uint8_t ESTIMATED_SIZE = 58; static constexpr uint8_t ESTIMATED_SIZE = 49;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_update_response"; } const char *message_name() const override { return "list_entities_update_response"; }
#endif #endif

File diff suppressed because it is too large Load Diff

View File

@ -598,32 +598,32 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
void APIServerConnection::on_hello_request(const HelloRequest &msg) { void APIServerConnection::on_hello_request(const HelloRequest &msg) {
HelloResponse ret = this->hello(msg); HelloResponse ret = this->hello(msg);
if (!this->send_message(ret)) { if (!this->send_message(ret, HelloResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
void APIServerConnection::on_connect_request(const ConnectRequest &msg) { void APIServerConnection::on_connect_request(const ConnectRequest &msg) {
ConnectResponse ret = this->connect(msg); ConnectResponse ret = this->connect(msg);
if (!this->send_message(ret)) { if (!this->send_message(ret, ConnectResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
DisconnectResponse ret = this->disconnect(msg); DisconnectResponse ret = this->disconnect(msg);
if (!this->send_message(ret)) { if (!this->send_message(ret, DisconnectResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
void APIServerConnection::on_ping_request(const PingRequest &msg) { void APIServerConnection::on_ping_request(const PingRequest &msg) {
PingResponse ret = this->ping(msg); PingResponse ret = this->ping(msg);
if (!this->send_message(ret)) { if (!this->send_message(ret, PingResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
if (this->check_connection_setup_()) { if (this->check_connection_setup_()) {
DeviceInfoResponse ret = this->device_info(msg); DeviceInfoResponse ret = this->device_info(msg);
if (!this->send_message(ret)) { if (!this->send_message(ret, DeviceInfoResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
@ -657,7 +657,7 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc
void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
if (this->check_connection_setup_()) { if (this->check_connection_setup_()) {
GetTimeResponse ret = this->get_time(msg); GetTimeResponse ret = this->get_time(msg);
if (!this->send_message(ret)) { if (!this->send_message(ret, GetTimeResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
@ -673,7 +673,7 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
if (this->check_authenticated_()) { if (this->check_authenticated_()) {
NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg);
if (!this->send_message(ret)) { if (!this->send_message(ret, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
@ -867,7 +867,7 @@ void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
const SubscribeBluetoothConnectionsFreeRequest &msg) { const SubscribeBluetoothConnectionsFreeRequest &msg) {
if (this->check_authenticated_()) { if (this->check_authenticated_()) {
BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg);
if (!this->send_message(ret)) { if (!this->send_message(ret, BluetoothConnectionsFreeResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
@ -899,7 +899,7 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
if (this->check_authenticated_()) { if (this->check_authenticated_()) {
VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg);
if (!this->send_message(ret)) { if (!this->send_message(ret, VoiceAssistantConfigurationResponse::MESSAGE_TYPE)) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }

View File

@ -18,11 +18,11 @@ class APIServerConnectionBase : public ProtoService {
public: public:
#endif #endif
template<typename T> bool send_message(const T &msg) { bool send_message(const ProtoMessage &msg, uint8_t message_type) {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_send_message_(msg.message_name(), msg.dump()); this->log_send_message_(msg.message_name(), msg.dump());
#endif #endif
return this->send_message_(msg, T::MESSAGE_TYPE); return this->send_message_(msg, message_type);
} }
virtual void on_hello_request(const HelloRequest &value){}; virtual void on_hello_request(const HelloRequest &value){};

View File

@ -31,7 +31,6 @@ APIServer::APIServer() {
} }
void APIServer::setup() { void APIServer::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
this->setup_controller(); this->setup_controller();
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@ -105,7 +104,7 @@ void APIServer::setup() {
return; return;
} }
for (auto &c : this->clients_) { for (auto &c : this->clients_) {
if (!c->flags_.remove) if (!c->flags_.remove && c->get_log_subscription_level() >= level)
c->try_send_log_message(level, tag, message, message_len); c->try_send_log_message(level, tag, message, message_len);
} }
}); });
@ -205,22 +204,20 @@ void APIServer::loop() {
void APIServer::dump_config() { void APIServer::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"API Server:\n" "Server:\n"
" Address: %s:%u", " Address: %s:%u",
network::get_use_address().c_str(), this->port_); network::get_use_address().c_str(), this->port_);
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Using noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
if (!this->noise_ctx_->has_psk()) { if (!this->noise_ctx_->has_psk()) {
ESP_LOGCONFIG(TAG, " Supports noise encryption: YES"); ESP_LOGCONFIG(TAG, " Supports encryption: YES");
} }
#else #else
ESP_LOGCONFIG(TAG, " Using noise encryption: NO"); ESP_LOGCONFIG(TAG, " Noise encryption: NO");
#endif #endif
} }
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
bool APIServer::uses_password() const { return !this->password_.empty(); }
bool APIServer::check_password(const std::string &password) const { bool APIServer::check_password(const std::string &password) const {
// depend only on input password length // depend only on input password length
const char *a = this->password_.c_str(); const char *a = this->password_.c_str();
@ -428,10 +425,11 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
ESP_LOGD(TAG, "Noise PSK saved"); ESP_LOGD(TAG, "Noise PSK saved");
if (make_active) { if (make_active) {
this->set_timeout(100, [this, psk]() { this->set_timeout(100, [this, psk]() {
ESP_LOGW(TAG, "Disconnecting all clients to reset connections"); ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
this->set_noise_psk(psk); this->set_noise_psk(psk);
for (auto &c : this->clients_) { for (auto &c : this->clients_) {
c->send_message(DisconnectRequest()); DisconnectRequest req;
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
} }
}); });
} }
@ -464,7 +462,8 @@ void APIServer::on_shutdown() {
// Send disconnect requests to all connected clients // Send disconnect requests to all connected clients
for (auto &c : this->clients_) { for (auto &c : this->clients_) {
if (!c->send_message(DisconnectRequest())) { DisconnectRequest req;
if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) {
// If we can't send the disconnect request directly (tx_buffer full), // 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 // schedule it at the front of the batch so it will be sent with priority
c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE, c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE,

View File

@ -39,7 +39,6 @@ class APIServer : public Component, public Controller {
bool teardown() override; bool teardown() override;
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
bool check_password(const std::string &password) const; bool check_password(const std::string &password) const;
bool uses_password() const;
void set_password(const std::string &password); void set_password(const std::string &password);
#endif #endif
void set_port(uint16_t port); void set_port(uint16_t port);

View File

@ -11,6 +11,18 @@ namespace esphome {
namespace api { namespace api {
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> { 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: public:
TemplatableStringValue() : TemplatableValue<std::string, X...>() {} TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
@ -19,7 +31,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
TemplatableStringValue(F f) TemplatableStringValue(F f)
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {} : TemplatableValue<std::string, X...>([f](X... x) -> std::string { return value_to_string(f(x...)); }) {}
}; };
template<typename... Ts> class TemplatableKeyValuePair { template<typename... Ts> class TemplatableKeyValuePair {

View File

@ -86,7 +86,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response(); auto resp = service->encode_list_service_response();
return this->client_->send_message(resp); return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE);
} }
#endif #endif

View File

@ -8,7 +8,7 @@ namespace api {
static const char *const TAG = "api.proto"; static const char *const TAG = "api.proto";
void ProtoMessage::decode(const uint8_t *buffer, size_t length) { void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
uint32_t i = 0; uint32_t i = 0;
bool error = false; bool error = false;
while (i < length) { while (i < length) {

View File

@ -135,6 +135,7 @@ class ProtoVarInt {
// Forward declaration for decode_to_message and encode_to_writer // Forward declaration for decode_to_message and encode_to_writer
class ProtoMessage; class ProtoMessage;
class ProtoDecodableMessage;
class ProtoLengthDelimited { class ProtoLengthDelimited {
public: public:
@ -142,15 +143,15 @@ class ProtoLengthDelimited {
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->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 ProtoMessage instance. * Decode the length-delimited data into an existing ProtoDecodableMessage instance.
* *
* This method allows decoding without templates, enabling use in contexts * This method allows decoding without templates, enabling use in contexts
* where the message type is not known at compile time. The ProtoMessage's * where the message type is not known at compile time. The ProtoDecodableMessage's
* decode() method will be called with the raw data and length. * decode() method will be called with the raw data and length.
* *
* @param msg The ProtoMessage instance to decode into * @param msg The ProtoDecodableMessage instance to decode into
*/ */
void decode_to_message(ProtoMessage &msg) const; void decode_to_message(ProtoDecodableMessage &msg) const;
protected: protected:
const uint8_t *const value_; const uint8_t *const value_;
@ -175,23 +176,7 @@ class Proto32Bit {
const uint32_t value_; const uint32_t value_;
}; };
class Proto64Bit { // NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
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 { class ProtoWriteBuffer {
public: public:
@ -205,9 +190,9 @@ class ProtoWriteBuffer {
* @param field_id Field number (tag) in the protobuf message * @param field_id Field number (tag) in the protobuf message
* @param type Wire type value: * @param type Wire type value:
* - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum) * - 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) * - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields)
* - 5: 32-bit (fixed32, sfixed32, float) * - 5: 32-bit (fixed32, sfixed32, float)
* - Note: Wire type 1 (64-bit fixed) is not supported
* *
* Following https://protobuf.dev/programming-guides/encoding/#structure * Following https://protobuf.dev/programming-guides/encoding/#structure
*/ */
@ -258,20 +243,10 @@ class ProtoWriteBuffer {
this->write((value >> 16) & 0xFF); this->write((value >> 16) & 0xFF);
this->write((value >> 24) & 0xFF); this->write((value >> 24) & 0xFF);
} }
void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) { // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
if (value == 0 && !force) // not supported to reduce overhead on embedded systems. All ESPHome devices are
return; // 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.
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);
}
void encode_float(uint32_t field_id, float value, bool force = false) { void encode_float(uint32_t field_id, float value, bool force = false) {
if (value == 0.0f && !force) if (value == 0.0f && !force)
return; return;
@ -324,7 +299,6 @@ class ProtoMessage {
virtual ~ProtoMessage() = default; virtual ~ProtoMessage() = default;
// Default implementation for messages with no fields // Default implementation for messages with no fields
virtual void encode(ProtoWriteBuffer buffer) const {} virtual void encode(ProtoWriteBuffer buffer) const {}
void decode(const uint8_t *buffer, size_t length);
// Default implementation for messages with no fields // Default implementation for messages with no fields
virtual void calculate_size(uint32_t &total_size) const {} virtual void calculate_size(uint32_t &total_size) const {}
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -332,12 +306,18 @@ class ProtoMessage {
virtual void dump_to(std::string &out) const = 0; virtual void dump_to(std::string &out) const = 0;
virtual const char *message_name() const { return "unknown"; } virtual const char *message_name() const { return "unknown"; }
#endif #endif
};
// Base class for messages that support decoding
class ProtoDecodableMessage : public ProtoMessage {
public:
void decode(const uint8_t *buffer, size_t length);
protected: protected:
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } 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_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; } // NOTE: decode_64bit removed - wire type 1 not supported
}; };
class ProtoSize { class ProtoSize {
@ -566,6 +546,42 @@ class ProtoSize {
total_size += field_id_size + 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 * @brief Calculates and adds the size of an enum field to the total message size
* *
@ -662,33 +678,8 @@ class ProtoSize {
total_size += field_id_size + varint(value); total_size += field_id_size + varint(value);
} }
/** // NOTE: sint64 support functions (add_sint64_field, add_sint64_field_repeated) removed
* @brief Calculates and adds the size of a sint64 field to the total message size // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
*
* Sint64 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
uint64_t zigzag = (static_cast<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 sint64 field to the total message size (repeated field version)
*
* Sint64 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
// Always calculate size for repeated fields
// ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
uint64_t zigzag = (static_cast<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 * @brief Calculates and adds the size of a string/bytes field to the total message size
@ -823,8 +814,8 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
} }
// Implementation of decode_to_message - must be after ProtoMessage is defined // Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
inline void ProtoLengthDelimited::decode_to_message(ProtoMessage &msg) const { inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) const {
msg.decode(this->value_, this->length_); msg.decode(this->value_, this->length_);
} }

View File

@ -16,6 +16,8 @@ class UserServiceDescriptor {
virtual ListEntitiesServicesResponse encode_list_service_response() = 0; virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 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); template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);

View File

@ -85,13 +85,13 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
cg.add(var.set_active(config[CONF_ACTIVE])) cg.add(var.set_active(config[CONF_ACTIVE]))
await esp32_ble_tracker.register_ble_device(var, config) await esp32_ble_tracker.register_raw_ble_device(var, config)
for connection_conf in config.get(CONF_CONNECTIONS, []): for connection_conf in config.get(CONF_CONNECTIONS, []):
connection_var = cg.new_Pvariable(connection_conf[CONF_ID]) connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
await cg.register_component(connection_var, connection_conf) await cg.register_component(connection_var, connection_conf)
cg.add(var.register_connection(connection_var)) cg.add(var.register_connection(connection_var))
await esp32_ble_tracker.register_client(connection_var, connection_conf) await esp32_ble_tracker.register_raw_client(connection_var, connection_conf)
if config.get(CONF_CACHE_SERVICES): if config.get(CONF_CACHE_SERVICES):
add_idf_sdkconfig_option("CONFIG_BT_GATTC_CACHE_NVS_FLASH", True) add_idf_sdkconfig_option("CONFIG_BT_GATTC_CACHE_NVS_FLASH", True)

View File

@ -75,7 +75,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
resp.data.reserve(param->read.value_len); resp.data.reserve(param->read.value_len);
// Use bulk insert instead of individual push_backs // Use bulk insert instead of individual push_backs
resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len); resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len);
this->proxy_->get_api_connection()->send_message(resp); this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE);
break; break;
} }
case ESP_GATTC_WRITE_CHAR_EVT: case ESP_GATTC_WRITE_CHAR_EVT:
@ -89,7 +89,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
api::BluetoothGATTWriteResponse resp; api::BluetoothGATTWriteResponse resp;
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->write.handle; resp.handle = param->write.handle;
this->proxy_->get_api_connection()->send_message(resp); this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE);
break; break;
} }
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
@ -103,7 +103,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
api::BluetoothGATTNotifyResponse resp; api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->unreg_for_notify.handle; resp.handle = param->unreg_for_notify.handle;
this->proxy_->get_api_connection()->send_message(resp); this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE);
break; break;
} }
case ESP_GATTC_REG_FOR_NOTIFY_EVT: { case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
@ -116,7 +116,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
api::BluetoothGATTNotifyResponse resp; api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->reg_for_notify.handle; resp.handle = param->reg_for_notify.handle;
this->proxy_->get_api_connection()->send_message(resp); this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE);
break; break;
} }
case ESP_GATTC_NOTIFY_EVT: { case ESP_GATTC_NOTIFY_EVT: {
@ -128,7 +128,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
resp.data.reserve(param->notify.value_len); resp.data.reserve(param->notify.value_len);
// Use bulk insert instead of individual push_backs // Use bulk insert instead of individual push_backs
resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len); resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len);
this->proxy_->get_api_connection()->send_message(resp); this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE);
break; break;
} }
default: default:

View File

@ -3,6 +3,7 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/macros.h" #include "esphome/core/macros.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include <cstring>
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -24,9 +25,30 @@ std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) {
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])}; ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])};
} }
// Batch size for BLE advertisements to maximize WiFi efficiency
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
// Most advertisements are 20-30 bytes, allowing even more to fit per packet
// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
// This achieves ~97% WiFi MTU utilization while staying under the limit
static constexpr size_t FLUSH_BATCH_SIZE = 16;
// Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response)
static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62,
"BLE advertisement data array size mismatch");
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
void BluetoothProxy::setup() { void BluetoothProxy::setup() {
// Pre-allocate response object
this->response_ = std::make_unique<api::BluetoothLERawAdvertisementsResponse>();
// Reserve capacity but start with size 0
// Reserve 50% since we'll grow naturally and flush at FLUSH_BATCH_SIZE
this->response_->advertisements.reserve(FLUSH_BATCH_SIZE / 2);
// Don't pre-allocate pool - let it grow only if needed in busy environments
// Many devices in quiet areas will never need the overflow pool
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
if (this->api_connection_ != nullptr) { if (this->api_connection_ != nullptr) {
this->send_bluetooth_scanner_state_(state); this->send_bluetooth_scanner_state_(state);
@ -39,83 +61,86 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
resp.state = static_cast<api::enums::BluetoothScannerState>(state); resp.state = static_cast<api::enums::BluetoothScannerState>(state);
resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
: api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE;
this->api_connection_->send_message(resp); this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE);
} }
#ifdef USE_ESP32_BLE_DEVICE
bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_) // This method should never be called since bluetooth_proxy always uses raw advertisements
return false; // but we need to provide an implementation to satisfy the virtual method requirement
return false;
ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(),
device.get_rssi());
this->send_api_packet_(device);
return true;
} }
#endif
// Batch size for BLE advertisements to maximize WiFi efficiency
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
// Most advertisements are 20-30 bytes, allowing even more to fit per packet
// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
// This achieves ~97% WiFi MTU utilization while staying under the limit
static constexpr size_t FLUSH_BATCH_SIZE = 16;
namespace {
// Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes)
// This is initialized at program startup before any threads
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
} // namespace
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { return batch_buffer; }
bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) { bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
return false; return false;
// Get the batch buffer reference auto &advertisements = this->response_->advertisements;
auto &batch_buffer = get_batch_buffer();
// Reserve additional capacity if needed
size_t new_size = batch_buffer.size() + count;
if (batch_buffer.capacity() < new_size) {
batch_buffer.reserve(new_size);
}
// Add new advertisements to the batch buffer
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
auto &result = scan_results[i]; auto &result = scan_results[i];
uint8_t length = result.adv_data_len + result.scan_rsp_len; uint8_t length = result.adv_data_len + result.scan_rsp_len;
batch_buffer.emplace_back(); // Check if we need to expand the vector
auto &adv = batch_buffer.back(); if (this->advertisement_count_ >= advertisements.size()) {
if (this->advertisement_pool_.empty()) {
// No room in pool, need to allocate
advertisements.emplace_back();
} else {
// Pull from pool
advertisements.push_back(std::move(this->advertisement_pool_.back()));
this->advertisement_pool_.pop_back();
}
}
// Fill in the data directly at current position
auto &adv = advertisements[this->advertisement_count_];
adv.address = esp32_ble::ble_addr_to_uint64(result.bda); adv.address = esp32_ble::ble_addr_to_uint64(result.bda);
adv.rssi = result.rssi; adv.rssi = result.rssi;
adv.address_type = result.ble_addr_type; adv.address_type = result.ble_addr_type;
adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]); adv.data_len = length;
std::memcpy(adv.data, result.ble_adv, length);
this->advertisement_count_++;
ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi);
}
// Only send if we've accumulated a good batch size to maximize batching efficiency // Flush if we have reached FLUSH_BATCH_SIZE
// https://github.com/esphome/backlog/issues/21 if (this->advertisement_count_ >= FLUSH_BATCH_SIZE) {
if (batch_buffer.size() >= FLUSH_BATCH_SIZE) { this->flush_pending_advertisements();
this->flush_pending_advertisements(); }
} }
return true; return true;
} }
void BluetoothProxy::flush_pending_advertisements() { void BluetoothProxy::flush_pending_advertisements() {
auto &batch_buffer = get_batch_buffer(); if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
return; return;
api::BluetoothLERawAdvertisementsResponse resp; auto &advertisements = this->response_->advertisements;
resp.advertisements.swap(batch_buffer);
this->api_connection_->send_message(resp); // Return any items beyond advertisement_count_ to the pool
if (advertisements.size() > this->advertisement_count_) {
// Move unused items back to pool
this->advertisement_pool_.insert(this->advertisement_pool_.end(),
std::make_move_iterator(advertisements.begin() + this->advertisement_count_),
std::make_move_iterator(advertisements.end()));
// Resize to actual count
advertisements.resize(this->advertisement_count_);
}
// Send the message
this->api_connection_->send_message(*this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE);
// Reset count - existing items will be overwritten in next batch
this->advertisement_count_ = 0;
} }
#ifdef USE_ESP32_BLE_DEVICE
void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) {
api::BluetoothLEAdvertisementResponse resp; api::BluetoothLEAdvertisementResponse resp;
resp.address = device.address_uint64(); resp.address = device.address_uint64();
@ -151,16 +176,16 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi
manufacturer_data.data.assign(data.data.begin(), data.data.end()); manufacturer_data.data.assign(data.data.begin(), data.data.end());
} }
this->api_connection_->send_message(resp); this->api_connection_->send_message(resp, api::BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
} }
#endif // USE_ESP32_BLE_DEVICE
void BluetoothProxy::dump_config() { void BluetoothProxy::dump_config() {
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" Active: %s\n" " Active: %s\n"
" Connections: %d\n" " Connections: %d",
" Raw advertisements: %s", YESNO(this->active_), this->connections_.size());
YESNO(this->active_), this->connections_.size(), YESNO(this->raw_advertisements_));
} }
int BluetoothProxy::get_bluetooth_connections_free() { int BluetoothProxy::get_bluetooth_connections_free() {
@ -188,15 +213,13 @@ void BluetoothProxy::loop() {
} }
// Flush any pending BLE advertisements that have been accumulated but not yet sent // Flush any pending BLE advertisements that have been accumulated but not yet sent
if (this->raw_advertisements_) { static uint32_t last_flush_time = 0;
static uint32_t last_flush_time = 0; uint32_t now = App.get_loop_component_start_time();
uint32_t now = App.get_loop_component_start_time();
// Flush accumulated advertisements every 100ms // Flush accumulated advertisements every 100ms
if (now - last_flush_time >= 100) { if (now - last_flush_time >= 100) {
this->flush_pending_advertisements(); this->flush_pending_advertisements();
last_flush_time = now; last_flush_time = now;
}
} }
for (auto *connection : this->connections_) { for (auto *connection : this->connections_) {
if (connection->send_service_ == connection->service_count_) { if (connection->send_service_ == connection->service_count_) {
@ -312,15 +335,13 @@ void BluetoothProxy::loop() {
service_resp.characteristics.push_back(std::move(characteristic_resp)); service_resp.characteristics.push_back(std::move(characteristic_resp));
} }
resp.services.push_back(std::move(service_resp)); resp.services.push_back(std::move(service_resp));
this->api_connection_->send_message(resp); this->api_connection_->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
} }
} }
} }
esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() { esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() {
if (this->raw_advertisements_) return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS;
return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS;
return esp32_ble_tracker::AdvertisementParserType::PARSED_ADVERTISEMENTS;
} }
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) { BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
@ -465,7 +486,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
call.success = ret == ESP_OK; call.success = ret == ESP_OK;
call.error = ret; call.error = ret;
this->api_connection_->send_message(call); this->api_connection_->send_message(call, api::BluetoothDeviceClearCacheResponse::MESSAGE_TYPE);
break; break;
} }
@ -565,7 +586,6 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection
return; return;
} }
this->api_connection_ = api_connection; this->api_connection_ = api_connection;
this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS;
this->parent_->recalculate_advertisement_parser_types(); this->parent_->recalculate_advertisement_parser_types();
this->send_bluetooth_scanner_state_(this->parent_->get_scanner_state()); this->send_bluetooth_scanner_state_(this->parent_->get_scanner_state());
@ -577,7 +597,6 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti
return; return;
} }
this->api_connection_ = nullptr; this->api_connection_ = nullptr;
this->raw_advertisements_ = false;
this->parent_->recalculate_advertisement_parser_types(); this->parent_->recalculate_advertisement_parser_types();
} }
@ -589,7 +608,7 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui
call.connected = connected; call.connected = connected;
call.mtu = mtu; call.mtu = mtu;
call.error = error; call.error = error;
this->api_connection_->send_message(call); this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE);
} }
void BluetoothProxy::send_connections_free() { void BluetoothProxy::send_connections_free() {
if (this->api_connection_ == nullptr) if (this->api_connection_ == nullptr)
@ -602,7 +621,7 @@ void BluetoothProxy::send_connections_free() {
call.allocated.push_back(connection->address_); call.allocated.push_back(connection->address_);
} }
} }
this->api_connection_->send_message(call); this->api_connection_->send_message(call, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
} }
void BluetoothProxy::send_gatt_services_done(uint64_t address) { void BluetoothProxy::send_gatt_services_done(uint64_t address) {
@ -610,7 +629,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) {
return; return;
api::BluetoothGATTGetServicesDoneResponse call; api::BluetoothGATTGetServicesDoneResponse call;
call.address = address; call.address = address;
this->api_connection_->send_message(call); this->api_connection_->send_message(call, api::BluetoothGATTGetServicesDoneResponse::MESSAGE_TYPE);
} }
void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) {
@ -620,7 +639,7 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_
call.address = address; call.address = address;
call.handle = handle; call.handle = handle;
call.error = error; call.error = error;
this->api_connection_->send_message(call); this->api_connection_->send_message(call, api::BluetoothGATTWriteResponse::MESSAGE_TYPE);
} }
void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) {
@ -629,7 +648,7 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_
call.paired = paired; call.paired = paired;
call.error = error; call.error = error;
this->api_connection_->send_message(call); this->api_connection_->send_message(call, api::BluetoothDevicePairingResponse::MESSAGE_TYPE);
} }
void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) {
@ -638,7 +657,7 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_e
call.success = success; call.success = success;
call.error = error; call.error = error;
this->api_connection_->send_message(call); this->api_connection_->send_message(call, api::BluetoothDeviceUnpairingResponse::MESSAGE_TYPE);
} }
void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {

View File

@ -51,7 +51,9 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
public: public:
BluetoothProxy(); BluetoothProxy();
#ifdef USE_ESP32_BLE_DEVICE
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
#endif
bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override; bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override;
void dump_config() override; void dump_config() override;
void setup() override; void setup() override;
@ -129,7 +131,9 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
} }
protected: protected:
#ifdef USE_ESP32_BLE_DEVICE
void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device);
#endif
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
BluetoothConnection *get_connection_(uint64_t address, bool reserve); BluetoothConnection *get_connection_(uint64_t address, bool reserve);
@ -141,9 +145,13 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
// Group 2: Container types (typically 12 bytes on 32-bit) // Group 2: Container types (typically 12 bytes on 32-bit)
std::vector<BluetoothConnection *> connections_{}; std::vector<BluetoothConnection *> connections_{};
// BLE advertisement batching
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_;
// Group 3: 1-byte types grouped together // Group 3: 1-byte types grouped together
bool active_; bool active_;
bool raw_advertisements_{false}; uint8_t advertisement_count_{0};
// 2 bytes used, 2 bytes padding // 2 bytes used, 2 bytes padding
}; };

View File

@ -3,6 +3,7 @@
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
CONF_BYTE_ORDER = "byte_order" CONF_BYTE_ORDER = "byte_order"
CONF_COLOR_DEPTH = "color_depth"
CONF_DRAW_ROUNDING = "draw_rounding" CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers" CONF_REQUEST_HEADERS = "request_headers"

View File

@ -17,6 +17,7 @@ from esphome.const import (
CONF_MODE, CONF_MODE,
CONF_NUMBER, CONF_NUMBER,
CONF_ON_VALUE, CONF_ON_VALUE,
CONF_SWITCH,
CONF_TEXT, CONF_TEXT,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_TYPE, CONF_TYPE,
@ -33,7 +34,6 @@ CONF_LABEL = "label"
CONF_MENU = "menu" CONF_MENU = "menu"
CONF_BACK = "back" CONF_BACK = "back"
CONF_SELECT = "select" CONF_SELECT = "select"
CONF_SWITCH = "switch"
CONF_ON_TEXT = "on_text" CONF_ON_TEXT = "on_text"
CONF_OFF_TEXT = "off_text" CONF_OFF_TEXT = "off_text"
CONF_VALUE_LAMBDA = "value_lambda" CONF_VALUE_LAMBDA = "value_lambda"

View File

@ -4,6 +4,7 @@
#include "esphome/components/network/ip_address.h" #include "esphome/components/network/ip_address.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
#include "esphome/core/helpers.h"
#include <lwip/igmp.h> #include <lwip/igmp.h>
#include <lwip/init.h> #include <lwip/init.h>
@ -71,7 +72,11 @@ bool E131Component::join_igmp_groups_() {
ip4_addr_t multicast_addr = ip4_addr_t multicast_addr =
network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)); network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff));
auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr); err_t err;
{
LwIPLock lock;
err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
}
if (err) { if (err) {
ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first); ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first);
@ -104,6 +109,7 @@ void E131Component::leave_(int universe) {
if (listen_method_ == E131_MULTICAST) { if (listen_method_ == E131_MULTICAST) {
ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)); ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff));
LwIPLock lock;
igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr); igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr);
} }

View File

@ -39,7 +39,7 @@ import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed
from esphome.types import ConfigType from esphome.types import ConfigType
from .boards import BOARDS from .boards import BOARDS, STANDARD_BOARDS
from .const import ( # noqa from .const import ( # noqa
KEY_BOARD, KEY_BOARD,
KEY_COMPONENTS, KEY_COMPONENTS,
@ -487,25 +487,32 @@ def _platform_is_platformio(value):
def _detect_variant(value): def _detect_variant(value):
board = value[CONF_BOARD] board = value.get(CONF_BOARD)
if board in BOARDS: variant = value.get(CONF_VARIANT)
variant = BOARDS[board][KEY_VARIANT] if variant and board is None:
if CONF_VARIANT in value and variant != value[CONF_VARIANT]: # If variant is set, we can derive the board from it
# variant has already been validated against the known set
value = value.copy()
value[CONF_BOARD] = STANDARD_BOARDS[variant]
elif board in BOARDS:
variant = variant or BOARDS[board][KEY_VARIANT]
if variant != BOARDS[board][KEY_VARIANT]:
raise cv.Invalid( raise cv.Invalid(
f"Option '{CONF_VARIANT}' does not match selected board.", f"Option '{CONF_VARIANT}' does not match selected board.",
path=[CONF_VARIANT], path=[CONF_VARIANT],
) )
value = value.copy() value = value.copy()
value[CONF_VARIANT] = variant value[CONF_VARIANT] = variant
elif not variant:
raise cv.Invalid(
"This board is unknown, if you are sure you want to compile with this board selection, "
f"override with option '{CONF_VARIANT}'",
path=[CONF_BOARD],
)
else: else:
if CONF_VARIANT not in value:
raise cv.Invalid(
"This board is unknown, if you are sure you want to compile with this board selection, "
f"override with option '{CONF_VARIANT}'",
path=[CONF_BOARD],
)
_LOGGER.warning( _LOGGER.warning(
"This board is unknown. Make sure the chosen chip component is correct.", "This board is unknown; the specified variant '%s' will be used but this may not work as expected.",
variant,
) )
return value return value
@ -676,7 +683,7 @@ CONF_PARTITIONS = "partitions"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.Required(CONF_BOARD): cv.string_strict, cv.Optional(CONF_BOARD): cv.string_strict,
cv.Optional(CONF_CPU_FREQUENCY): cv.one_of( cv.Optional(CONF_CPU_FREQUENCY): cv.one_of(
*FULL_CPU_FREQUENCIES, upper=True *FULL_CPU_FREQUENCIES, upper=True
), ),
@ -691,6 +698,7 @@ CONFIG_SCHEMA = cv.All(
_detect_variant, _detect_variant,
_set_default_framework, _set_default_framework,
set_core_data, set_core_data,
cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT),
) )

View File

@ -2,13 +2,30 @@ from .const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4, VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
VARIANTS,
) )
STANDARD_BOARDS = {
VARIANT_ESP32: "esp32dev",
VARIANT_ESP32C2: "esp32-c2-devkitm-1",
VARIANT_ESP32C3: "esp32-c3-devkitm-1",
VARIANT_ESP32C5: "esp32-c5-devkitc-1",
VARIANT_ESP32C6: "esp32-c6-devkitm-1",
VARIANT_ESP32H2: "esp32-h2-devkitm-1",
VARIANT_ESP32P4: "esp32-p4-evboard",
VARIANT_ESP32S2: "esp32-s2-kaluga-1",
VARIANT_ESP32S3: "esp32-s3-devkitc-1",
}
# Make sure not missed here if a new variant added.
assert all(v in STANDARD_BOARDS for v in VARIANTS)
ESP32_BASE_PINS = { ESP32_BASE_PINS = {
"TX": 1, "TX": 1,
"RX": 3, "RX": 3,

View File

@ -1,4 +1,5 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/defines.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -30,6 +31,45 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
#include "lwip/priv/tcpip_priv.h"
#endif
LwIPLock::LwIPLock() {
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
// When CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled, lwIP uses a global mutex to protect
// its internal state. Any thread can take this lock to safely access lwIP APIs.
//
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) returns true if the current thread
// already holds the lwIP core lock. This prevents recursive locking attempts and
// allows nested LwIPLock instances to work correctly.
//
// If we don't already hold the lock, acquire it. This will block until the lock
// is available if another thread currently holds it.
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
LOCK_TCPIP_CORE();
}
#endif
}
LwIPLock::~LwIPLock() {
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
// Only release the lwIP core lock if this thread currently holds it.
//
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) queries lwIP's internal lock
// ownership tracking. It returns true only if the current thread is registered
// as the lock holder.
//
// This check is essential because:
// 1. We may not have acquired the lock in the constructor (if we already held it)
// 2. The lock might have been released by other means between constructor and destructor
// 3. Calling UNLOCK_TCPIP_CORE() without holding the lock causes undefined behavior
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
UNLOCK_TCPIP_CORE();
}
#endif
}
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED) #if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default

View File

@ -105,6 +105,7 @@ void BLEClientBase::dump_config() {
} }
} }
#ifdef USE_ESP32_BLE_DEVICE
bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
if (!this->auto_connect_) if (!this->auto_connect_)
return false; return false;
@ -122,6 +123,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
this->remote_addr_type_ = device.get_address_type(); this->remote_addr_type_ = device.get_address_type();
return true; return true;
} }
#endif
void BLEClientBase::connect() { void BLEClientBase::connect() {
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(), ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(),

View File

@ -31,7 +31,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void dump_config() override; void dump_config() override;
void run_later(std::function<void()> &&f); // NOLINT void run_later(std::function<void()> &&f); // NOLINT
#ifdef USE_ESP32_BLE_DEVICE
bool parse_device(const espbt::ESPBTDevice &device) override; bool parse_device(const espbt::ESPBTDevice &device) override;
#endif
void on_scan_end() override {} void on_scan_end() override {}
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override; esp_ble_gattc_cb_param_t *param) override;

View File

@ -31,6 +31,8 @@ from esphome.const import (
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
) )
from esphome.core import CORE from esphome.core import CORE
from esphome.enum import StrEnum
from esphome.types import ConfigType
AUTO_LOAD = ["esp32_ble"] AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
@ -50,6 +52,25 @@ IDF_MAX_CONNECTIONS = 9
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Enum for BLE features
class BLEFeatures(StrEnum):
ESP_BT_DEVICE = "ESP_BT_DEVICE"
# Set to track which features are needed by components
_required_features: set[BLEFeatures] = set()
def register_ble_features(features: set[BLEFeatures]) -> None:
"""Register BLE features that a component needs.
Args:
features: Set of BLEFeatures enum members
"""
_required_features.update(features)
esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker")
ESP32BLETracker = esp32_ble_tracker_ns.class_( ESP32BLETracker = esp32_ble_tracker_ns.class_(
"ESP32BLETracker", "ESP32BLETracker",
@ -277,6 +298,15 @@ async def to_code(config):
cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625))) cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625)))
cg.add(var.set_scan_active(params[CONF_ACTIVE])) cg.add(var.set_scan_active(params[CONF_ACTIVE]))
cg.add(var.set_scan_continuous(params[CONF_CONTINUOUS])) cg.add(var.set_scan_continuous(params[CONF_CONTINUOUS]))
# Register ESP_BT_DEVICE feature if any of the automation triggers are used
if (
config.get(CONF_ON_BLE_ADVERTISE)
or config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE)
or config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE)
):
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
for conf in config.get(CONF_ON_BLE_ADVERTISE, []): for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_MAC_ADDRESS in conf: if CONF_MAC_ADDRESS in conf:
@ -334,6 +364,11 @@ async def to_code(config):
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT") cg.add_define("USE_ESP32_BLE_CLIENT")
# Add feature-specific defines based on what's needed
if BLEFeatures.ESP_BT_DEVICE in _required_features:
cg.add_define("USE_ESP32_BLE_DEVICE")
if config.get(CONF_SOFTWARE_COEXISTENCE): if config.get(CONF_SOFTWARE_COEXISTENCE):
cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE") cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE")
@ -382,13 +417,43 @@ async def esp32_ble_tracker_stop_scan_action_to_code(
return var return var
async def register_ble_device(var, config): async def register_ble_device(
var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var)) cg.add(paren.register_listener(var))
return var return var
async def register_client(var, config): async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var
async def register_raw_ble_device(
var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType:
"""Register a BLE device listener that only needs raw advertisement data.
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
async def register_raw_client(
var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType:
"""Register a BLE client that only needs raw advertisement data.
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var)) cg.add(paren.register_client(var))
return var return var

View File

@ -7,6 +7,7 @@
namespace esphome { namespace esphome {
namespace esp32_ble_tracker { namespace esp32_ble_tracker {
#ifdef USE_ESP32_BLE_DEVICE
class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener { class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener {
public: public:
explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); }
@ -87,6 +88,7 @@ class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener {
bool parse_device(const ESPBTDevice &device) override { return false; } bool parse_device(const ESPBTDevice &device) override { return false; }
void on_scan_end() override { this->trigger(); } void on_scan_end() override { this->trigger(); }
}; };
#endif // USE_ESP32_BLE_DEVICE
template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> { template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> {
public: public:

View File

@ -141,6 +141,7 @@ void ESP32BLETracker::loop() {
} }
if (this->parse_advertisements_) { if (this->parse_advertisements_) {
#ifdef USE_ESP32_BLE_DEVICE
ESPBTDevice device; ESPBTDevice device;
device.parse_scan_rst(scan_result); device.parse_scan_rst(scan_result);
@ -162,6 +163,7 @@ void ESP32BLETracker::loop() {
if (!found && !this->scan_continuous_) { if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device); this->print_bt_device_info(device);
} }
#endif // USE_ESP32_BLE_DEVICE
} }
// Move to next entry in ring buffer // Move to next entry in ring buffer
@ -511,6 +513,7 @@ void ESP32BLETracker::set_scanner_state_(ScannerState state) {
this->scanner_state_callbacks_.call(state); this->scanner_state_callbacks_.call(state);
} }
#ifdef USE_ESP32_BLE_DEVICE
ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); } ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); }
optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) { optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) {
if (!data.uuid.contains(0x4C, 0x00)) if (!data.uuid.contains(0x4C, 0x00))
@ -751,13 +754,16 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) {
} }
} }
} }
std::string ESPBTDevice::address_str() const { std::string ESPBTDevice::address_str() const {
char mac[24]; char mac[24];
snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", this->address_[0], this->address_[1], this->address_[2], snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", this->address_[0], this->address_[1], this->address_[2],
this->address_[3], this->address_[4], this->address_[5]); this->address_[3], this->address_[4], this->address_[5]);
return mac; return mac;
} }
uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); } uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); }
#endif // USE_ESP32_BLE_DEVICE
void ESP32BLETracker::dump_config() { void ESP32BLETracker::dump_config() {
ESP_LOGCONFIG(TAG, "BLE Tracker:"); ESP_LOGCONFIG(TAG, "BLE Tracker:");
@ -796,6 +802,7 @@ void ESP32BLETracker::dump_config() {
} }
} }
#ifdef USE_ESP32_BLE_DEVICE
void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) {
const uint64_t address = device.address_uint64(); const uint64_t address = device.address_uint64();
for (auto &disc : this->already_discovered_) { for (auto &disc : this->already_discovered_) {
@ -866,8 +873,9 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) && return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
} }
#endif // USE_ESP32_BLE_DEVICE
} // namespace esp32_ble_tracker } // namespace esp32_ble_tracker
} // namespace esphome } // namespace esphome
#endif #endif // USE_ESP32

View File

@ -39,6 +39,7 @@ struct ServiceData {
adv_data_t data; adv_data_t data;
}; };
#ifdef USE_ESP32_BLE_DEVICE
class ESPBLEiBeacon { class ESPBLEiBeacon {
public: public:
ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); } ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); }
@ -116,13 +117,16 @@ class ESPBTDevice {
std::vector<ServiceData> service_datas_{}; std::vector<ServiceData> service_datas_{};
const BLEScanResult *scan_result_{nullptr}; const BLEScanResult *scan_result_{nullptr};
}; };
#endif // USE_ESP32_BLE_DEVICE
class ESP32BLETracker; class ESP32BLETracker;
class ESPBTDeviceListener { class ESPBTDeviceListener {
public: public:
virtual void on_scan_end() {} virtual void on_scan_end() {}
#ifdef USE_ESP32_BLE_DEVICE
virtual bool parse_device(const ESPBTDevice &device) = 0; virtual bool parse_device(const ESPBTDevice &device) = 0;
#endif
virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; }; virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; };
virtual AdvertisementParserType get_advertisement_parser_type() { virtual AdvertisementParserType get_advertisement_parser_type() {
return AdvertisementParserType::PARSED_ADVERTISEMENTS; return AdvertisementParserType::PARSED_ADVERTISEMENTS;
@ -237,7 +241,9 @@ class ESP32BLETracker : public Component,
void register_client(ESPBTClient *client); void register_client(ESPBTClient *client);
void recalculate_advertisement_parser_types(); void recalculate_advertisement_parser_types();
#ifdef USE_ESP32_BLE_DEVICE
void print_bt_device_info(const ESPBTDevice &device); void print_bt_device_info(const ESPBTDevice &device);
#endif
void start_scan(); void start_scan();
void stop_scan(); void stop_scan();

View File

@ -1,3 +1,5 @@
import logging
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import i2c from esphome.components import i2c
@ -8,6 +10,7 @@ from esphome.const import (
CONF_CONTRAST, CONF_CONTRAST,
CONF_DATA_PINS, CONF_DATA_PINS,
CONF_FREQUENCY, CONF_FREQUENCY,
CONF_I2C,
CONF_I2C_ID, CONF_I2C_ID,
CONF_ID, CONF_ID,
CONF_PIN, CONF_PIN,
@ -20,6 +23,9 @@ from esphome.const import (
) )
from esphome.core import CORE from esphome.core import CORE
from esphome.core.entity_helpers import setup_entity from esphome.core.entity_helpers import setup_entity
import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
@ -113,6 +119,12 @@ ENUM_SPECIAL_EFFECT = {
"SEPIA": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_SEPIA, "SEPIA": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_SEPIA,
} }
camera_fb_location_t = cg.global_ns.enum("camera_fb_location_t")
ENUM_FB_LOCATION = {
"PSRAM": cg.global_ns.CAMERA_FB_IN_PSRAM,
"DRAM": cg.global_ns.CAMERA_FB_IN_DRAM,
}
# pin assignment # pin assignment
CONF_HREF_PIN = "href_pin" CONF_HREF_PIN = "href_pin"
CONF_PIXEL_CLOCK_PIN = "pixel_clock_pin" CONF_PIXEL_CLOCK_PIN = "pixel_clock_pin"
@ -143,6 +155,7 @@ CONF_MAX_FRAMERATE = "max_framerate"
CONF_IDLE_FRAMERATE = "idle_framerate" CONF_IDLE_FRAMERATE = "idle_framerate"
# frame buffer # frame buffer
CONF_FRAME_BUFFER_COUNT = "frame_buffer_count" CONF_FRAME_BUFFER_COUNT = "frame_buffer_count"
CONF_FRAME_BUFFER_LOCATION = "frame_buffer_location"
# stream trigger # stream trigger
CONF_ON_STREAM_START = "on_stream_start" CONF_ON_STREAM_START = "on_stream_start"
@ -224,6 +237,9 @@ CONFIG_SCHEMA = cv.All(
cv.framerate, cv.Range(min=0, max=1) cv.framerate, cv.Range(min=0, max=1)
), ),
cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2),
cv.Optional(CONF_FRAME_BUFFER_LOCATION, default="PSRAM"): cv.enum(
ENUM_FB_LOCATION, upper=True
),
cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
@ -250,6 +266,22 @@ CONFIG_SCHEMA = cv.All(
cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
) )
def _final_validate(config):
if CONF_I2C_PINS not in config:
return
fconf = fv.full_config.get()
if fconf.get(CONF_I2C):
raise cv.Invalid(
"The `i2c_pins:` config option is incompatible with an dedicated `i2c:` block, use `i2c_id` instead"
)
_LOGGER.warning(
"The `i2c_pins:` config option is deprecated. Use `i2c_id:` with a dedicated `i2c:` definition instead."
)
FINAL_VALIDATE_SCHEMA = _final_validate
SETTERS = { SETTERS = {
# pin assignment # pin assignment
CONF_DATA_PINS: "set_data_pins", CONF_DATA_PINS: "set_data_pins",
@ -279,6 +311,7 @@ SETTERS = {
CONF_WB_MODE: "set_wb_mode", CONF_WB_MODE: "set_wb_mode",
# test pattern # test pattern
CONF_TEST_PATTERN: "set_test_pattern", CONF_TEST_PATTERN: "set_test_pattern",
CONF_FRAME_BUFFER_LOCATION: "set_frame_buffer_location",
} }
@ -306,6 +339,7 @@ async def to_code(config):
else: else:
cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE])) cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE]))
cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT])) cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT]))
cg.add(var.set_frame_buffer_location(config[CONF_FRAME_BUFFER_LOCATION]))
cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add(var.set_frame_size(config[CONF_RESOLUTION]))
cg.add_define("USE_CAMERA") cg.add_define("USE_CAMERA")

View File

@ -133,6 +133,7 @@ void ESP32Camera::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" JPEG Quality: %u\n" " JPEG Quality: %u\n"
" Framebuffer Count: %u\n" " Framebuffer Count: %u\n"
" Framebuffer Location: %s\n"
" Contrast: %d\n" " Contrast: %d\n"
" Brightness: %d\n" " Brightness: %d\n"
" Saturation: %d\n" " Saturation: %d\n"
@ -140,8 +141,9 @@ void ESP32Camera::dump_config() {
" Horizontal Mirror: %s\n" " Horizontal Mirror: %s\n"
" Special Effect: %u\n" " Special Effect: %u\n"
" White Balance Mode: %u", " White Balance Mode: %u",
st.quality, conf.fb_count, st.contrast, st.brightness, st.saturation, ONOFF(st.vflip), st.quality, conf.fb_count, this->config_.fb_location == CAMERA_FB_IN_PSRAM ? "PSRAM" : "DRAM",
ONOFF(st.hmirror), st.special_effect, st.wb_mode); st.contrast, st.brightness, st.saturation, ONOFF(st.vflip), ONOFF(st.hmirror), st.special_effect,
st.wb_mode);
// ESP_LOGCONFIG(TAG, " Auto White Balance: %u", st.awb); // ESP_LOGCONFIG(TAG, " Auto White Balance: %u", st.awb);
// ESP_LOGCONFIG(TAG, " Auto White Balance Gain: %u", st.awb_gain); // ESP_LOGCONFIG(TAG, " Auto White Balance Gain: %u", st.awb_gain);
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
@ -350,6 +352,9 @@ void ESP32Camera::set_frame_buffer_count(uint8_t fb_count) {
this->config_.fb_count = fb_count; this->config_.fb_count = fb_count;
this->set_frame_buffer_mode(fb_count > 1 ? CAMERA_GRAB_LATEST : CAMERA_GRAB_WHEN_EMPTY); this->set_frame_buffer_mode(fb_count > 1 ? CAMERA_GRAB_LATEST : CAMERA_GRAB_WHEN_EMPTY);
} }
void ESP32Camera::set_frame_buffer_location(camera_fb_location_t fb_location) {
this->config_.fb_location = fb_location;
}
/* ---------------- public API (specific) ---------------- */ /* ---------------- public API (specific) ---------------- */
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<camera::CameraImage>)> &&callback) { void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<camera::CameraImage>)> &&callback) {

View File

@ -152,6 +152,7 @@ class ESP32Camera : public camera::Camera {
/* -- frame buffer */ /* -- frame buffer */
void set_frame_buffer_mode(camera_grab_mode_t mode); void set_frame_buffer_mode(camera_grab_mode_t mode);
void set_frame_buffer_count(uint8_t fb_count); void set_frame_buffer_count(uint8_t fb_count);
void set_frame_buffer_location(camera_fb_location_t fb_location);
/* public API (derivated) */ /* public API (derivated) */
void setup() override; void setup() override;

View File

@ -22,6 +22,10 @@ void Mutex::unlock() {}
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
// ESP8266 doesn't support lwIP core locking, so this is a no-op
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
wifi_get_macaddr(STATION_IF, mac); wifi_get_macaddr(STATION_IF, mac);
} }

View File

@ -420,6 +420,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() {
} }
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) { network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
LwIPLock lock;
const ip_addr_t *dns_ip = dns_getserver(num); const ip_addr_t *dns_ip = dns_getserver(num);
return dns_ip; return dns_ip;
} }
@ -527,6 +528,7 @@ void EthernetComponent::start_connect_() {
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
if (this->manual_ip_.has_value()) { if (this->manual_ip_.has_value()) {
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) { if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d; ip_addr_t d;
d = this->manual_ip_->dns1; d = this->manual_ip_->dns1;
@ -559,8 +561,13 @@ bool EthernetComponent::is_connected() { return this->state_ == EthernetComponen
void EthernetComponent::dump_connect_params_() { void EthernetComponent::dump_connect_params_() {
esp_netif_ip_info_t ip; esp_netif_ip_info_t ip;
esp_netif_get_ip_info(this->eth_netif_, &ip); esp_netif_get_ip_info(this->eth_netif_, &ip);
const ip_addr_t *dns_ip1 = dns_getserver(0); const ip_addr_t *dns_ip1;
const ip_addr_t *dns_ip2 = dns_getserver(1); const ip_addr_t *dns_ip2;
{
LwIPLock lock;
dns_ip1 = dns_getserver(0);
dns_ip2 = dns_getserver(1);
}
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" IP Address: %s\n" " IP Address: %s\n"

View File

@ -29,7 +29,6 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS
} }
float get_setup_priority() const override { return setup_priority::ETHERNET; } float get_setup_priority() const override { return setup_priority::ETHERNET; }
std::string unique_id() override { return get_mac_address() + "-ethernetinfo"; }
void dump_config() override; void dump_config() override;
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
@ -52,7 +51,6 @@ class DNSAddressEthernetInfo : public PollingComponent, public text_sensor::Text
} }
} }
float get_setup_priority() const override { return setup_priority::ETHERNET; } float get_setup_priority() const override { return setup_priority::ETHERNET; }
std::string unique_id() override { return get_mac_address() + "-ethernetinfo-dns"; }
void dump_config() override; void dump_config() override;
protected: protected:
@ -63,7 +61,6 @@ class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor
public: public:
void setup() override { this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty()); } void setup() override { this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty()); }
float get_setup_priority() const override { return setup_priority::ETHERNET; } float get_setup_priority() const override { return setup_priority::ETHERNET; }
std::string unique_id() override { return get_mac_address() + "-ethernetinfo-mac"; }
void dump_config() override; void dump_config() override;
}; };

View File

@ -29,7 +29,21 @@ CONFIG_SCHEMA = (
.extend( .extend(
{ {
cv.Required(CONF_PIN): pins.gpio_input_pin_schema, cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean, # Interrupts are disabled by default for bk72xx, ln882x, and rtl87xx platforms
# due to hardware limitations or lack of reliable interrupt support. This ensures
# stable operation on these platforms. Future maintainers should verify platform
# capabilities before changing this default behavior.
cv.SplitDefault(
CONF_USE_INTERRUPT,
bk72xx=False,
esp32=True,
esp8266=True,
host=True,
ln882x=False,
nrf52=True,
rp2040=True,
rtl87xx=False,
): cv.boolean,
cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum( cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
INTERRUPT_TYPES, upper=True INTERRUPT_TYPES, upper=True
), ),

View File

@ -7,6 +7,7 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <driver/gpio.h>
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0)
#define SOC_HP_I2C_NUM SOC_I2C_NUM #define SOC_HP_I2C_NUM SOC_I2C_NUM
@ -20,21 +21,72 @@ static const char *const TAG = "i2c.idf";
void IDFI2CBus::setup() { void IDFI2CBus::setup() {
ESP_LOGCONFIG(TAG, "Running setup"); ESP_LOGCONFIG(TAG, "Running setup");
static i2c_port_t next_port = I2C_NUM_0; static i2c_port_t next_port = I2C_NUM_0;
port_ = next_port; this->port_ = next_port;
if (this->port_ == I2C_NUM_MAX) {
ESP_LOGE(TAG, "No more than %u buses supported", I2C_NUM_MAX);
this->mark_failed();
return;
}
if (this->timeout_ > 13000) {
ESP_LOGW(TAG, "Using max allowed timeout: 13 ms");
this->timeout_ = 13000;
}
this->recover_();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
next_port = (i2c_port_t) (next_port + 1);
i2c_master_bus_config_t bus_conf{};
memset(&bus_conf, 0, sizeof(bus_conf));
bus_conf.sda_io_num = gpio_num_t(sda_pin_);
bus_conf.scl_io_num = gpio_num_t(scl_pin_);
bus_conf.i2c_port = this->port_;
bus_conf.glitch_ignore_cnt = 7;
#if SOC_LP_I2C_SUPPORTED
if (this->port_ < SOC_HP_I2C_NUM) {
bus_conf.clk_source = I2C_CLK_SRC_DEFAULT;
} else {
bus_conf.lp_source_clk = LP_I2C_SCLK_DEFAULT;
}
#else
bus_conf.clk_source = I2C_CLK_SRC_DEFAULT;
#endif
bus_conf.flags.enable_internal_pullup = sda_pullup_enabled_ || scl_pullup_enabled_;
esp_err_t err = i2c_new_master_bus(&bus_conf, &this->bus_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
i2c_device_config_t dev_conf{};
memset(&dev_conf, 0, sizeof(dev_conf));
dev_conf.dev_addr_length = I2C_ADDR_BIT_LEN_7;
dev_conf.device_address = I2C_DEVICE_ADDRESS_NOT_USED;
dev_conf.scl_speed_hz = this->frequency_;
dev_conf.scl_wait_us = this->timeout_;
err = i2c_master_bus_add_device(this->bus_, &dev_conf, &this->dev_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
this->initialized_ = true;
if (this->scan_) {
ESP_LOGV(TAG, "Scanning for devices");
this->i2c_scan_();
}
#else
#if SOC_HP_I2C_NUM > 1 #if SOC_HP_I2C_NUM > 1
next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX; next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX;
#else #else
next_port = I2C_NUM_MAX; next_port = I2C_NUM_MAX;
#endif #endif
if (port_ == I2C_NUM_MAX) {
ESP_LOGE(TAG, "No more than %u buses supported", SOC_HP_I2C_NUM);
this->mark_failed();
return;
}
recover_();
i2c_config_t conf{}; i2c_config_t conf{};
memset(&conf, 0, sizeof(conf)); memset(&conf, 0, sizeof(conf));
conf.mode = I2C_MODE_MASTER; conf.mode = I2C_MODE_MASTER;
@ -53,11 +105,7 @@ void IDFI2CBus::setup() {
this->mark_failed(); this->mark_failed();
return; return;
} }
if (timeout_ > 0) { // if timeout specified in yaml: if (timeout_ > 0) {
if (timeout_ > 13000) {
ESP_LOGW(TAG, "i2c timeout of %" PRIu32 "us greater than max of 13ms on esp-idf, setting to max", timeout_);
timeout_ = 13000;
}
err = i2c_set_timeout(port_, timeout_ * 80); // unit: APB 80MHz clock cycle err = i2c_set_timeout(port_, timeout_ * 80); // unit: APB 80MHz clock cycle
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_set_timeout failed: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "i2c_set_timeout failed: %s", esp_err_to_name(err));
@ -73,12 +121,15 @@ void IDFI2CBus::setup() {
this->mark_failed(); this->mark_failed();
return; return;
} }
initialized_ = true; initialized_ = true;
if (this->scan_) { if (this->scan_) {
ESP_LOGV(TAG, "Scanning bus for active devices"); ESP_LOGV(TAG, "Scanning bus for active devices");
this->i2c_scan_(); this->i2c_scan_();
} }
#endif
} }
void IDFI2CBus::dump_config() { void IDFI2CBus::dump_config() {
ESP_LOGCONFIG(TAG, "I2C Bus:"); ESP_LOGCONFIG(TAG, "I2C Bus:");
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
@ -123,6 +174,74 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) {
ESP_LOGVV(TAG, "i2c bus not initialized!"); ESP_LOGVV(TAG, "i2c bus not initialized!");
return ERROR_NOT_INITIALIZED; return ERROR_NOT_INITIALIZED;
} }
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
i2c_operation_job_t jobs[cnt + 4];
uint8_t read = (address << 1) | I2C_MASTER_READ;
size_t last = 0, num = 0;
jobs[num].command = I2C_MASTER_CMD_START;
num++;
jobs[num].command = I2C_MASTER_CMD_WRITE;
jobs[num].write.ack_check = true;
jobs[num].write.data = &read;
jobs[num].write.total_bytes = 1;
num++;
// find the last valid index
for (size_t i = 0; i < cnt; i++) {
const auto &buf = buffers[i];
if (buf.len == 0) {
continue;
}
last = i;
}
for (size_t i = 0; i < cnt; i++) {
const auto &buf = buffers[i];
if (buf.len == 0) {
continue;
}
if (i == last) {
// the last byte read before stop should always be a nack,
// split the last read if len is larger than 1
if (buf.len > 1) {
jobs[num].command = I2C_MASTER_CMD_READ;
jobs[num].read.ack_value = I2C_ACK_VAL;
jobs[num].read.data = (uint8_t *) buf.data;
jobs[num].read.total_bytes = buf.len - 1;
num++;
}
jobs[num].command = I2C_MASTER_CMD_READ;
jobs[num].read.ack_value = I2C_NACK_VAL;
jobs[num].read.data = (uint8_t *) buf.data + buf.len - 1;
jobs[num].read.total_bytes = 1;
num++;
} else {
jobs[num].command = I2C_MASTER_CMD_READ;
jobs[num].read.ack_value = I2C_ACK_VAL;
jobs[num].read.data = (uint8_t *) buf.data;
jobs[num].read.total_bytes = buf.len;
num++;
}
}
jobs[num].command = I2C_MASTER_CMD_STOP;
num++;
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20);
if (err == ESP_ERR_INVALID_STATE) {
ESP_LOGVV(TAG, "RX from %02X failed: not acked", address);
return ERROR_NOT_ACKNOWLEDGED;
} else if (err == ESP_ERR_TIMEOUT) {
ESP_LOGVV(TAG, "RX from %02X failed: timeout", address);
return ERROR_TIMEOUT;
} else if (err != ESP_OK) {
ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err));
return ERROR_UNKNOWN;
}
#else
i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_cmd_handle_t cmd = i2c_cmd_link_create();
esp_err_t err = i2c_master_start(cmd); esp_err_t err = i2c_master_start(cmd);
if (err != ESP_OK) { if (err != ESP_OK) {
@ -168,6 +287,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) {
ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err)); ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err));
return ERROR_UNKNOWN; return ERROR_UNKNOWN;
} }
#endif
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char debug_buf[4]; char debug_buf[4];
@ -185,6 +305,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) {
return ERROR_OK; return ERROR_OK;
} }
ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) {
// logging is only enabled with vv level, if warnings are shown the caller // logging is only enabled with vv level, if warnings are shown the caller
// should log them // should log them
@ -207,6 +328,49 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b
ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str()); ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str());
#endif #endif
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
i2c_operation_job_t jobs[cnt + 3];
uint8_t write = (address << 1) | I2C_MASTER_WRITE;
size_t num = 0;
jobs[num].command = I2C_MASTER_CMD_START;
num++;
jobs[num].command = I2C_MASTER_CMD_WRITE;
jobs[num].write.ack_check = true;
jobs[num].write.data = &write;
jobs[num].write.total_bytes = 1;
num++;
for (size_t i = 0; i < cnt; i++) {
const auto &buf = buffers[i];
if (buf.len == 0) {
continue;
}
jobs[num].command = I2C_MASTER_CMD_WRITE;
jobs[num].write.ack_check = true;
jobs[num].write.data = (uint8_t *) buf.data;
jobs[num].write.total_bytes = buf.len;
num++;
}
if (stop) {
jobs[num].command = I2C_MASTER_CMD_STOP;
num++;
}
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20);
if (err == ESP_ERR_INVALID_STATE) {
ESP_LOGVV(TAG, "TX to %02X failed: not acked", address);
return ERROR_NOT_ACKNOWLEDGED;
} else if (err == ESP_ERR_TIMEOUT) {
ESP_LOGVV(TAG, "TX to %02X failed: timeout", address);
return ERROR_TIMEOUT;
} else if (err != ESP_OK) {
ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err));
return ERROR_UNKNOWN;
}
#else
i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_cmd_handle_t cmd = i2c_cmd_link_create();
esp_err_t err = i2c_master_start(cmd); esp_err_t err = i2c_master_start(cmd);
if (err != ESP_OK) { if (err != ESP_OK) {
@ -252,6 +416,7 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b
ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err));
return ERROR_UNKNOWN; return ERROR_UNKNOWN;
} }
#endif
return ERROR_OK; return ERROR_OK;
} }

View File

@ -2,9 +2,14 @@
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
#include <driver/i2c.h>
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "i2c_bus.h" #include "i2c_bus.h"
#include "esp_idf_version.h"
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
#include <driver/i2c_master.h>
#else
#include <driver/i2c.h>
#endif
namespace esphome { namespace esphome {
namespace i2c { namespace i2c {
@ -38,6 +43,10 @@ class IDFI2CBus : public InternalI2CBus, public Component {
RecoveryCode recovery_result_; RecoveryCode recovery_result_;
protected: protected:
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
i2c_master_dev_handle_t dev_;
i2c_master_bus_handle_t bus_;
#endif
i2c_port_t port_; i2c_port_t port_;
uint8_t sda_pin_; uint8_t sda_pin_;
bool sda_pullup_enabled_; bool sda_pullup_enabled_;

View File

@ -36,8 +36,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
#ifdef USE_I2S_LEGACY #ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_ADC #if SOC_I2S_SUPPORTS_ADC
void set_adc_channel(adc1_channel_t channel) { void set_adc_channel(adc_channel_t channel) {
this->adc_channel_ = channel; this->adc_channel_ = (adc1_channel_t) channel;
this->adc_ = true; this->adc_ = true;
} }
#endif #endif

View File

@ -26,6 +26,10 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
// LibreTiny doesn't support lwIP core locking, so this is a no-op
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
WiFi.macAddress(mac); WiFi.macAddress(mac);
} }

View File

@ -21,6 +21,11 @@ from esphome.components.libretiny.const import (
COMPONENT_LN882X, COMPONENT_LN882X,
COMPONENT_RTL87XX, COMPONENT_RTL87XX,
) )
from esphome.components.zephyr import (
zephyr_add_cdc_acm,
zephyr_add_overlay,
zephyr_add_prj_conf,
)
from esphome.config_helpers import filter_source_files_from_platform from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@ -41,6 +46,7 @@ from esphome.const import (
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_LN882X, PLATFORM_LN882X,
PLATFORM_NRF52,
PLATFORM_RP2040, PLATFORM_RP2040,
PLATFORM_RTL87XX, PLATFORM_RTL87XX,
PlatformFramework, PlatformFramework,
@ -115,6 +121,8 @@ ESP_ARDUINO_UNSUPPORTED_USB_UARTS = [USB_SERIAL_JTAG]
UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1]
UART_SELECTION_NRF52 = [USB_CDC, UART0]
HARDWARE_UART_TO_UART_SELECTION = { HARDWARE_UART_TO_UART_SELECTION = {
UART0: logger_ns.UART_SELECTION_UART0, UART0: logger_ns.UART_SELECTION_UART0,
UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP,
@ -167,6 +175,8 @@ def uart_selection(value):
return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value) return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value)
if CORE.is_host: if CORE.is_host:
raise cv.Invalid("Uart selection not valid for host platform") raise cv.Invalid("Uart selection not valid for host platform")
if CORE.is_nrf52:
return cv.one_of(*UART_SELECTION_NRF52, upper=True)(value)
raise NotImplementedError raise NotImplementedError
@ -183,9 +193,10 @@ def validate_local_no_higher_than_global(value):
Logger = logger_ns.class_("Logger", cg.Component) Logger = logger_ns.class_("Logger", cg.Component)
LoggerMessageTrigger = logger_ns.class_( LoggerMessageTrigger = logger_ns.class_(
"LoggerMessageTrigger", "LoggerMessageTrigger",
automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr), automation.Trigger.template(cg.uint8, cg.const_char_ptr, cg.const_char_ptr),
) )
CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash" CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = "esp8266_store_log_strings_in_flash"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
@ -227,6 +238,7 @@ CONFIG_SCHEMA = cv.All(
bk72xx=DEFAULT, bk72xx=DEFAULT,
ln882x=DEFAULT, ln882x=DEFAULT,
rtl87xx=DEFAULT, rtl87xx=DEFAULT,
nrf52=USB_CDC,
): cv.All( ): cv.All(
cv.only_on( cv.only_on(
[ [
@ -236,6 +248,7 @@ CONFIG_SCHEMA = cv.All(
PLATFORM_BK72XX, PLATFORM_BK72XX,
PLATFORM_LN882X, PLATFORM_LN882X,
PLATFORM_RTL87XX, PLATFORM_RTL87XX,
PLATFORM_NRF52,
] ]
), ),
uart_selection, uart_selection,
@ -358,6 +371,15 @@ async def to_code(config):
except cv.Invalid: except cv.Invalid:
pass pass
if CORE.using_zephyr:
if config[CONF_HARDWARE_UART] == UART0:
zephyr_add_overlay("""&uart0 { status = "okay";};""")
if config[CONF_HARDWARE_UART] == UART1:
zephyr_add_overlay("""&uart1 { status = "okay";};""")
if config[CONF_HARDWARE_UART] == USB_CDC:
zephyr_add_prj_conf("UART_LINE_CTRL", True)
zephyr_add_cdc_acm(config, 0)
# Register at end for safe mode # Register at end for safe mode
await cg.register_component(log, config) await cg.register_component(log, config)
@ -368,7 +390,7 @@ async def to_code(config):
await automation.build_automation( await automation.build_automation(
trigger, trigger,
[ [
(cg.int_, "level"), (cg.uint8, "level"),
(cg.const_char_ptr, "tag"), (cg.const_char_ptr, "tag"),
(cg.const_char_ptr, "message"), (cg.const_char_ptr, "message"),
], ],
@ -462,6 +484,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO, PlatformFramework.LN882X_ARDUINO,
}, },
"logger_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
"task_log_buffer.cpp": { "task_log_buffer.cpp": {
PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_IDF,

View File

@ -4,9 +4,9 @@
#include <memory> // For unique_ptr #include <memory> // For unique_ptr
#endif #endif
#include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace logger { namespace logger {
@ -160,6 +160,8 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
this->main_task_ = xTaskGetCurrentTaskHandle(); this->main_task_ = xTaskGetCurrentTaskHandle();
#elif defined(USE_ZEPHYR)
this->main_task_ = k_current_get();
#endif #endif
} }
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
@ -172,6 +174,7 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
} }
#endif #endif
#ifndef USE_ZEPHYR
#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32)
void Logger::loop() { void Logger::loop() {
#if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) #if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO)
@ -185,8 +188,13 @@ void Logger::loop() {
} }
opened = !opened; opened = !opened;
} }
#endif
this->process_messages_();
}
#endif
#endif #endif
void Logger::process_messages_() {
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
// Process any buffered messages when available // Process any buffered messages when available
if (this->log_buffer_->has_messages()) { if (this->log_buffer_->has_messages()) {
@ -227,12 +235,11 @@ void Logger::loop() {
} }
#endif #endif
} }
#endif
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
UARTSelection Logger::get_uart() const { return this->uart_; } UARTSelection Logger::get_uart() const { return this->uart_; }
#endif #endif

View File

@ -29,6 +29,11 @@
#include <driver/uart.h> #include <driver/uart.h>
#endif // USE_ESP_IDF #endif // USE_ESP_IDF
#ifdef USE_ZEPHYR
#include <zephyr/kernel.h>
struct device;
#endif
namespace esphome { namespace esphome {
namespace logger { namespace logger {
@ -56,7 +61,7 @@ static const char *const LOG_LEVEL_LETTERS[] = {
"VV", // VERY_VERBOSE "VV", // VERY_VERBOSE
}; };
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
/** Enum for logging UART selection /** Enum for logging UART selection
* *
* Advanced configuration (pin selection, etc) is not supported. * Advanced configuration (pin selection, etc) is not supported.
@ -82,7 +87,7 @@ enum UARTSelection : uint8_t {
UART_SELECTION_UART0_SWAP, UART_SELECTION_UART0_SWAP,
#endif // USE_ESP8266 #endif // USE_ESP8266
}; };
#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY #endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY || USE_ZEPHYR
/** /**
* @brief Logger component for all ESPHome logging. * @brief Logger component for all ESPHome logging.
@ -107,7 +112,7 @@ class Logger : public Component {
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
void init_log_buffer(size_t total_buffer_size); void init_log_buffer(size_t total_buffer_size);
#endif #endif
#if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) #if defined(USE_LOGGER_USB_CDC) || defined(USE_ESP32) || defined(USE_ZEPHYR)
void loop() override; void loop() override;
#endif #endif
/// Manually set the baud rate for serial, set to 0 to disable. /// Manually set the baud rate for serial, set to 0 to disable.
@ -122,7 +127,7 @@ class Logger : public Component {
#ifdef USE_ESP32 #ifdef USE_ESP32
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
#endif #endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
/// Get the UART used by the logger. /// Get the UART used by the logger.
UARTSelection get_uart() const; UARTSelection get_uart() const;
@ -157,6 +162,7 @@ class Logger : public Component {
#endif #endif
protected: protected:
void process_messages_();
void write_msg_(const char *msg); void write_msg_(const char *msg);
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
@ -164,7 +170,7 @@ class Logger : public Component {
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
va_list args, char *buffer, uint16_t *buffer_at, va_list args, char *buffer, uint16_t *buffer_at,
uint16_t buffer_size) { uint16_t buffer_size) {
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size);
#else #else
this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size);
@ -231,7 +237,10 @@ class Logger : public Component {
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
Stream *hw_serial_{nullptr}; Stream *hw_serial_{nullptr};
#endif #endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ZEPHYR)
const device *uart_dev_{nullptr};
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
void *main_task_ = nullptr; // Only used for thread name identification void *main_task_ = nullptr; // Only used for thread name identification
#endif #endif
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -256,7 +265,7 @@ class Logger : public Component {
uint16_t tx_buffer_at_{0}; uint16_t tx_buffer_at_{0};
uint16_t tx_buffer_size_{0}; uint16_t tx_buffer_size_{0};
uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE};
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR)
UARTSelection uart_{UART_SELECTION_UART0}; UARTSelection uart_{UART_SELECTION_UART0};
#endif #endif
#ifdef USE_LIBRETINY #ifdef USE_LIBRETINY
@ -268,9 +277,13 @@ class Logger : public Component {
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
#endif #endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
const char *HOT get_thread_name_() { const char *HOT get_thread_name_() {
#ifdef USE_ZEPHYR
k_tid_t current_task = k_current_get();
#else
TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
#endif
if (current_task == main_task_) { if (current_task == main_task_) {
return nullptr; // Main task return nullptr; // Main task
} else { } else {
@ -278,6 +291,8 @@ class Logger : public Component {
return pcTaskGetName(current_task); return pcTaskGetName(current_task);
#elif defined(USE_LIBRETINY) #elif defined(USE_LIBRETINY)
return pcTaskGetTaskName(current_task); return pcTaskGetTaskName(current_task);
#elif defined(USE_ZEPHYR)
return k_thread_name_get(current_task);
#endif #endif
} }
} }
@ -319,7 +334,7 @@ class Logger : public Component {
const char *color = esphome::logger::LOG_LEVEL_COLORS[level]; const char *color = esphome::logger::LOG_LEVEL_COLORS[level];
const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level]; const char *letter = esphome::logger::LOG_LEVEL_LETTERS[level];
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
if (thread_name != nullptr) { if (thread_name != nullptr) {
// Non-main task with thread name // Non-main task with thread name
this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line, this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line,

View File

@ -0,0 +1,88 @@
#ifdef USE_ZEPHYR
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "logger.h"
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/usb/usb_device.h>
namespace esphome {
namespace logger {
static const char *const TAG = "logger";
void Logger::loop() {
#ifdef USE_LOGGER_USB_CDC
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
return;
}
static bool opened = false;
uint32_t dtr = 0;
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
/* Poll if the DTR flag was set, optional */
if (opened == dtr) {
return;
}
if (!opened) {
App.schedule_dump_config();
}
opened = !opened;
#endif
this->process_messages_();
}
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
static const struct device *uart_dev = nullptr;
switch (this->uart_) {
case UART_SELECTION_UART0:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0));
break;
case UART_SELECTION_UART1:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
break;
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
if (device_is_ready(uart_dev)) {
usb_enable(nullptr);
}
break;
#endif
}
if (!device_is_ready(uart_dev)) {
ESP_LOGE(TAG, "%s is not ready.", get_uart_selection_());
} else {
this->uart_dev_ = uart_dev;
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) {
#ifdef CONFIG_PRINTK
printk("%s\n", msg);
#endif
if (nullptr == this->uart_dev_) {
return;
}
while (*msg) {
uart_poll_out(this->uart_dev_, *msg);
++msg;
}
uart_poll_out(this->uart_dev_, '\n');
}
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"};
const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; }
} // namespace logger
} // namespace esphome
#endif

View File

@ -76,6 +76,7 @@ async def theme_to_code(config):
for w_name, style in theme.items(): for w_name, style in theme.items():
# Work around Python 3.10 bug with nested async comprehensions # Work around Python 3.10 bug with nested async comprehensions
# With Python 3.11 this could be simplified # With Python 3.11 this could be simplified
# TODO: Now that we require Python 3.11+, this can be updated to use nested comprehensions
styles = {} styles = {}
for part, states in collect_parts(style).items(): for part, states in collect_parts(style).items():
styles[part] = { styles[part] = {

View File

@ -192,7 +192,7 @@ class WidgetType:
class NumberType(WidgetType): class NumberType(WidgetType):
def get_max(self, config: dict): def get_max(self, config: dict):
return int(config[CONF_MAX_VALUE] or 100) return int(config.get(CONF_MAX_VALUE, 100))
def get_min(self, config: dict): def get_min(self, config: dict):
return int(config[CONF_MIN_VALUE] or 0) return int(config.get(CONF_MIN_VALUE, 0))

View File

@ -14,6 +14,7 @@ from esphome.const import (
CONF_VALUE, CONF_VALUE,
CONF_WIDTH, CONF_WIDTH,
) )
from esphome.cpp_generator import IntLiteral
from ..automation import action_to_code from ..automation import action_to_code
from ..defines import ( from ..defines import (
@ -188,6 +189,8 @@ class MeterType(WidgetType):
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
if CONF_ROTATION in scale_conf: if CONF_ROTATION in scale_conf:
rotation = await lv_angle.process(scale_conf[CONF_ROTATION]) rotation = await lv_angle.process(scale_conf[CONF_ROTATION])
if isinstance(rotation, IntLiteral):
rotation = int(str(rotation)) // 10
with LocalVariable( with LocalVariable(
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
) as meter_var: ) as meter_var:
@ -264,7 +267,7 @@ class MeterType(WidgetType):
color_start, color_start,
color_end, color_end,
v[CONF_LOCAL], v[CONF_LOCAL],
size.process(v[CONF_WIDTH]), await size.process(v[CONF_WIDTH]),
), ),
) )
if t == CONF_IMAGE: if t == CONF_IMAGE:

View File

@ -1,9 +1,9 @@
from esphome.const import CONF_SWITCH
from ..defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN from ..defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN
from ..types import LvBoolean from ..types import LvBoolean
from . import WidgetType from . import WidgetType
CONF_SWITCH = "switch"
class SwitchType(WidgetType): class SwitchType(WidgetType):
def __init__(self): def __init__(self):

View File

@ -2,10 +2,8 @@ CODEOWNERS = ["@clydebarrow"]
DOMAIN = "mipi_spi" DOMAIN = "mipi_spi"
CONF_DRAW_FROM_ORIGIN = "draw_from_origin"
CONF_SPI_16 = "spi_16" CONF_SPI_16 = "spi_16"
CONF_PIXEL_MODE = "pixel_mode" CONF_PIXEL_MODE = "pixel_mode"
CONF_COLOR_DEPTH = "color_depth"
CONF_BUS_MODE = "bus_mode" CONF_BUS_MODE = "bus_mode"
CONF_USE_AXIS_FLIPS = "use_axis_flips" CONF_USE_AXIS_FLIPS = "use_axis_flips"
CONF_NATIVE_WIDTH = "native_width" CONF_NATIVE_WIDTH = "native_width"

View File

@ -3,11 +3,18 @@ import logging
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import display, spi from esphome.components import display, spi
from esphome.components.const import (
CONF_BYTE_ORDER,
CONF_COLOR_DEPTH,
CONF_DRAW_ROUNDING,
)
from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.config_validation import ALLOW_EXTRA from esphome.config_validation import ALLOW_EXTRA
from esphome.const import ( from esphome.const import (
CONF_BRIGHTNESS, CONF_BRIGHTNESS,
CONF_BUFFER_SIZE,
CONF_COLOR_ORDER, CONF_COLOR_ORDER,
CONF_CS_PIN, CONF_CS_PIN,
CONF_DATA_RATE, CONF_DATA_RATE,
@ -24,19 +31,19 @@ from esphome.const import (
CONF_MODEL, CONF_MODEL,
CONF_OFFSET_HEIGHT, CONF_OFFSET_HEIGHT,
CONF_OFFSET_WIDTH, CONF_OFFSET_WIDTH,
CONF_PAGES,
CONF_RESET_PIN, CONF_RESET_PIN,
CONF_ROTATION, CONF_ROTATION,
CONF_SWAP_XY, CONF_SWAP_XY,
CONF_TRANSFORM, CONF_TRANSFORM,
CONF_WIDTH, CONF_WIDTH,
) )
from esphome.core import TimePeriod from esphome.core import CORE, TimePeriod
from esphome.cpp_generator import TemplateArguments
from esphome.final_validate import full_config
from ..const import CONF_DRAW_ROUNDING
from ..lvgl.defines import CONF_COLOR_DEPTH
from . import ( from . import (
CONF_BUS_MODE, CONF_BUS_MODE,
CONF_DRAW_FROM_ORIGIN,
CONF_NATIVE_HEIGHT, CONF_NATIVE_HEIGHT,
CONF_NATIVE_WIDTH, CONF_NATIVE_WIDTH,
CONF_PIXEL_MODE, CONF_PIXEL_MODE,
@ -55,6 +62,7 @@ from .models import (
MADCTL_XFLIP, MADCTL_XFLIP,
MADCTL_YFLIP, MADCTL_YFLIP,
DriverChip, DriverChip,
adafruit,
amoled, amoled,
cyd, cyd,
ili, ili,
@ -69,43 +77,112 @@ DEPENDENCIES = ["spi"]
LOGGER = logging.getLogger(DOMAIN) LOGGER = logging.getLogger(DOMAIN)
mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi") mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi")
MipiSpi = mipi_spi_ns.class_( MipiSpi = mipi_spi_ns.class_("MipiSpi", display.Display, cg.Component, spi.SPIDevice)
"MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice MipiSpiBuffer = mipi_spi_ns.class_(
"MipiSpiBuffer", MipiSpi, display.Display, cg.Component, spi.SPIDevice
) )
ColorOrder = display.display_ns.enum("ColorMode") ColorOrder = display.display_ns.enum("ColorMode")
ColorBitness = display.display_ns.enum("ColorBitness") ColorBitness = display.display_ns.enum("ColorBitness")
Model = mipi_spi_ns.enum("Model") Model = mipi_spi_ns.enum("Model")
PixelMode = mipi_spi_ns.enum("PixelMode")
BusType = mipi_spi_ns.enum("BusType")
COLOR_ORDERS = { COLOR_ORDERS = {
MODE_RGB: ColorOrder.COLOR_ORDER_RGB, MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
MODE_BGR: ColorOrder.COLOR_ORDER_BGR, MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
} }
COLOR_DEPTHS = { COLOR_DEPTHS = {
8: ColorBitness.COLOR_BITNESS_332, 8: PixelMode.PIXEL_MODE_8,
16: ColorBitness.COLOR_BITNESS_565, 16: PixelMode.PIXEL_MODE_16,
18: PixelMode.PIXEL_MODE_18,
} }
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
BusTypes = {
TYPE_SINGLE: BusType.BUS_TYPE_SINGLE,
TYPE_QUAD: BusType.BUS_TYPE_QUAD,
TYPE_OCTAL: BusType.BUS_TYPE_OCTAL,
}
DriverChip("CUSTOM", initsequence={}) DriverChip("CUSTOM")
MODELS = DriverChip.models MODELS = DriverChip.models
# These statements are noops, but serve to suppress linting of side-effect-only imports # This loop is a noop, but suppresses linting of side-effect-only imports
for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare): for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare, adafruit):
pass pass
PixelMode = mipi_spi_ns.enum("PixelMode")
PIXEL_MODE_18BIT = "18bit" DISPLAY_18BIT = "18bit"
PIXEL_MODE_16BIT = "16bit" DISPLAY_16BIT = "16bit"
PIXEL_MODES = { DISPLAY_PIXEL_MODES = {
PIXEL_MODE_16BIT: 0x55, DISPLAY_16BIT: (0x55, PixelMode.PIXEL_MODE_16),
PIXEL_MODE_18BIT: 0x66, DISPLAY_18BIT: (0x66, PixelMode.PIXEL_MODE_18),
} }
def get_dimensions(config):
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
offset_width = dimensions[CONF_OFFSET_WIDTH]
offset_height = dimensions[CONF_OFFSET_HEIGHT]
return width, height, offset_width, offset_height
(width, height) = dimensions
return width, height, 0, 0
# Default dimensions, use model defaults
transform = get_transform(config)
model = MODELS[config[CONF_MODEL]]
width = model.get_default(CONF_WIDTH)
height = model.get_default(CONF_HEIGHT)
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
# the offset is asymmetric
if transform[CONF_MIRROR_X]:
native_width = model.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2)
offset_width = native_width - width - offset_width
if transform[CONF_MIRROR_Y]:
native_height = model.get_default(
CONF_NATIVE_HEIGHT, height + offset_height * 2
)
offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set
if transform[CONF_SWAP_XY] is True:
width, height = height, width
offset_height, offset_width = offset_width, offset_height
return width, height, offset_width, offset_height
def denominator(config):
"""
Calculate the best denominator for a buffer size fraction.
The denominator must be a number between 2 and 16 that divides the display height evenly,
and the fraction represented by the denominator must be less than or equal to the given fraction.
:config: The configuration dictionary containing the buffer size fraction and display dimensions
:return: The denominator to use for the buffer size fraction
"""
frac = config.get(CONF_BUFFER_SIZE)
if frac is None or frac > 0.75:
return 1
height, _width, _offset_width, _offset_height = get_dimensions(config)
try:
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
except StopIteration:
raise cv.Invalid(
f"Buffer size fraction {frac} is not compatible with display height {height}"
) from StopIteration
def validate_dimension(rounding): def validate_dimension(rounding):
def validator(value): def validator(value):
value = cv.positive_int(value) value = cv.positive_int(value)
@ -158,41 +235,50 @@ def dimension_schema(rounding):
) )
def model_schema(bus_mode, model: DriverChip, swapsies: bool): def swap_xy_schema(model):
uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
def validator(value):
if value:
raise cv.Invalid("Axis swapping not supported by this model")
return cv.boolean(value)
if uses_swap:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
transform = cv.Schema( transform = cv.Schema(
{ {
cv.Required(CONF_MIRROR_X): cv.boolean, cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean, cv.Required(CONF_MIRROR_Y): cv.boolean,
**swap_xy_schema(model),
} }
) )
if model.get_default(CONF_SWAP_XY, False) == cv.UNDEFINED:
transform = transform.extend(
{
cv.Optional(CONF_SWAP_XY): cv.invalid(
"Axis swapping not supported by this model"
)
}
)
else:
transform = transform.extend(
{
cv.Required(CONF_SWAP_XY): cv.boolean,
}
)
# CUSTOM model will need to provide a custom init sequence # CUSTOM model will need to provide a custom init sequence
iseqconf = ( iseqconf = (
cv.Required(CONF_INIT_SEQUENCE) cv.Required(CONF_INIT_SEQUENCE)
if model.initsequence is None if model.initsequence is None
else cv.Optional(CONF_INIT_SEQUENCE) else cv.Optional(CONF_INIT_SEQUENCE)
) )
# Dimensions are optional if the model has a default width and the transform is not overridden # Dimensions are optional if the model has a default width and the x-y transform is not overridden
is_swapped = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True
cv_dimensions = ( cv_dimensions = (
cv.Optional if model.get_default(CONF_WIDTH) and not swapsies else cv.Required cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required
) )
pixel_modes = PIXEL_MODES if bus_mode == TYPE_SINGLE else (PIXEL_MODE_16BIT,) pixel_modes = DISPLAY_PIXEL_MODES if bus_mode == TYPE_SINGLE else (DISPLAY_16BIT,)
color_depth = ( color_depth = (
("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit") ("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit")
) )
other_options = [
CONF_INVERT_COLORS,
CONF_USE_AXIS_FLIPS,
]
if bus_mode == TYPE_SINGLE:
other_options.append(CONF_SPI_16)
schema = ( schema = (
display.FULL_DISPLAY_SCHEMA.extend( display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema( spi.spi_device_schema(
@ -220,11 +306,13 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum( model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(
COLOR_ORDERS, upper=True COLOR_ORDERS, upper=True
), ),
model.option(CONF_BYTE_ORDER, "big_endian"): cv.one_of(
"big_endian", "little_endian", lower=True
),
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True), model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
model.option(CONF_DRAW_ROUNDING, 2): power_of_two, model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any( model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of(
cv.one_of(*pixel_modes, lower=True), *pixel_modes, lower=True
cv.int_range(0, 255, min_included=True, max_included=True),
), ),
cv.Optional(CONF_TRANSFORM): transform, cv.Optional(CONF_TRANSFORM): transform,
cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of( cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of(
@ -232,19 +320,12 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
), ),
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
iseqconf: cv.ensure_list(map_sequence), iseqconf: cv.ensure_list(map_sequence),
cv.Optional(CONF_BUFFER_SIZE): cv.All(
cv.percentage, cv.Range(0.12, 1.0)
),
} }
) )
.extend( .extend({model.option(x): cv.boolean for x in other_options})
{
model.option(x): cv.boolean
for x in [
CONF_DRAW_FROM_ORIGIN,
CONF_SPI_16,
CONF_INVERT_COLORS,
CONF_USE_AXIS_FLIPS,
]
}
)
) )
if brightness := model.get_default(CONF_BRIGHTNESS): if brightness := model.get_default(CONF_BRIGHTNESS):
schema = schema.extend( schema = schema.extend(
@ -259,18 +340,25 @@ def model_schema(bus_mode, model: DriverChip, swapsies: bool):
return schema return schema
def rotation_as_transform(model, config): def is_rotation_transformable(config):
""" """
Check if a rotation can be implemented in hardware using the MADCTL register. Check if a rotation can be implemented in hardware using the MADCTL register.
A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y.
""" """
model = MODELS[config[CONF_MODEL]]
rotation = config.get(CONF_ROTATION, 0) rotation = config.get(CONF_ROTATION, 0)
return rotation and ( return rotation and (
model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180
) )
def config_schema(config): def customise_schema(config):
"""
Create a customised config schema for a specific model and validate the configuration.
:param config: The configuration dictionary to validate
:return: The validated configuration dictionary
:raises cv.Invalid: If the configuration is invalid
"""
# First get the model and bus mode # First get the model and bus mode
config = cv.Schema( config = cv.Schema(
{ {
@ -288,29 +376,94 @@ def config_schema(config):
extra=ALLOW_EXTRA, extra=ALLOW_EXTRA,
)(config) )(config)
bus_mode = config.get(CONF_BUS_MODE, model.modes[0]) bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True config = model_schema(config)(config)
config = model_schema(bus_mode, model, swapsies)(config)
# Check for invalid combinations of MADCTL config # Check for invalid combinations of MADCTL config
if init_sequence := config.get(CONF_INIT_SEQUENCE): if init_sequence := config.get(CONF_INIT_SEQUENCE):
if MADCTL in [x[0] for x in init_sequence] and CONF_TRANSFORM in config: commands = [x[0] for x in init_sequence]
if MADCTL in commands and CONF_TRANSFORM in config:
raise cv.Invalid( raise cv.Invalid(
f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence" f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence"
) )
if PIXFMT in commands:
raise cv.Invalid(
f"PIXFMT ({PIXFMT:#X}) should not be in the init sequence, it will be set automatically"
)
if bus_mode == TYPE_QUAD and CONF_DC_PIN in config: if bus_mode == TYPE_QUAD and CONF_DC_PIN in config:
raise cv.Invalid("DC pin is not supported in quad mode") raise cv.Invalid("DC pin is not supported in quad mode")
if config[CONF_PIXEL_MODE] == PIXEL_MODE_18BIT and bus_mode != TYPE_SINGLE:
raise cv.Invalid("18-bit pixel mode is not supported on a quad or octal bus")
if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config: if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config:
raise cv.Invalid(f"DC pin is required in {bus_mode} mode") raise cv.Invalid(f"DC pin is required in {bus_mode} mode")
denominator(config)
return config return config
CONFIG_SCHEMA = config_schema CONFIG_SCHEMA = customise_schema
def get_transform(model, config): def requires_buffer(config):
can_transform = rotation_as_transform(model, config) """
Check if the display configuration requires a buffer. It will do so if any drawing methods are configured.
:param config:
:return: True if a buffer is required, False otherwise
"""
return any(
config.get(key) for key in (CONF_LAMBDA, CONF_PAGES, CONF_SHOW_TEST_CARD)
)
def get_color_depth(config):
return int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
def _final_validate(config):
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
if not requires_buffer(config) and LVGL_DOMAIN not in global_config:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
if "psram" not in global_config and CONF_BUFFER_SIZE not in config:
if not requires_buffer(config):
return config # No buffer needed, so no need to set a buffer size
# If PSRAM is not enabled, choose a small buffer size by default
if not requires_buffer(config):
# not our problem.
return config
color_depth = get_color_depth(config)
frac = denominator(config)
height, width, _offset_width, _offset_height = get_dimensions(config)
buffer_size = color_depth // 8 * width * height // frac
# Target a buffer size of 20kB
fraction = 20000.0 / buffer_size
try:
config[CONF_BUFFER_SIZE] = 1.0 / next(
x for x in range(2, 17) if fraction >= 1 / x and height % x == 0
)
except StopIteration:
# Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0
# PSRAM will be needed.
if CORE.is_esp32:
raise cv.Invalid(
"PSRAM is required for this display"
) from StopIteration
return config
FINAL_VALIDATE_SCHEMA = _final_validate
def get_transform(config):
"""
Get the transformation configuration for the display.
:param config:
:return:
"""
model = MODELS[config[CONF_MODEL]]
can_transform = is_rotation_transformable(config)
transform = config.get( transform = config.get(
CONF_TRANSFORM, CONF_TRANSFORM,
{ {
@ -350,16 +503,13 @@ def get_sequence(model, config):
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence] sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
commands = [x[0] for x in sequence] commands = [x[0] for x in sequence]
# Set pixel format if not already in the custom sequence # Set pixel format if not already in the custom sequence
if PIXFMT not in commands: pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]]
pixel_mode = config[CONF_PIXEL_MODE] sequence.append((PIXFMT, pixel_mode[0]))
if not isinstance(pixel_mode, int):
pixel_mode = PIXEL_MODES[pixel_mode]
sequence.append((PIXFMT, pixel_mode))
# Does the chip use the flipping bits for mirroring rather than the reverse order bits? # Does the chip use the flipping bits for mirroring rather than the reverse order bits?
use_flip = config[CONF_USE_AXIS_FLIPS] use_flip = config[CONF_USE_AXIS_FLIPS]
if MADCTL not in commands: if MADCTL not in commands:
madctl = 0 madctl = 0
transform = get_transform(model, config) transform = get_transform(config)
if transform.get(CONF_TRANSFORM): if transform.get(CONF_TRANSFORM):
LOGGER.info("Using hardware transform to implement rotation") LOGGER.info("Using hardware transform to implement rotation")
if transform.get(CONF_MIRROR_X): if transform.get(CONF_MIRROR_X):
@ -396,63 +546,62 @@ def get_sequence(model, config):
) )
def get_instance(config):
"""
Get the type of MipiSpi instance to create based on the configuration,
and the template arguments.
:param config:
:return: type, template arguments
"""
width, height, offset_width, offset_height = get_dimensions(config)
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
bufferpixels = COLOR_DEPTHS[color_depth]
display_pixel_mode = DISPLAY_PIXEL_MODES[config[CONF_PIXEL_MODE]][1]
bus_type = config[CONF_BUS_MODE]
if bus_type == TYPE_SINGLE and config.get(CONF_SPI_16, False):
# If the bus mode is single and spi_16 is set, use single 16-bit mode
bus_type = BusType.BUS_TYPE_SINGLE_16
else:
bus_type = BusTypes[bus_type]
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
frac = denominator(config)
rotation = DISPLAY_ROTATIONS[
0 if is_rotation_transformable(config) else config.get(CONF_ROTATION, 0)
]
templateargs = [
buffer_type,
bufferpixels,
config[CONF_BYTE_ORDER] == "big_endian",
display_pixel_mode,
bus_type,
width,
height,
offset_width,
offset_height,
]
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
if requires_buffer(config):
templateargs.append(rotation)
templateargs.append(frac)
return MipiSpiBuffer, templateargs
return MipiSpi, templateargs
async def to_code(config): async def to_code(config):
model = MODELS[config[CONF_MODEL]] model = MODELS[config[CONF_MODEL]]
transform = get_transform(model, config) var_id = config[CONF_ID]
if CONF_DIMENSIONS in config: var_id.type, templateargs = get_instance(config)
# Explicit dimensions, just use as is var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
offset_width = dimensions[CONF_OFFSET_WIDTH]
offset_height = dimensions[CONF_OFFSET_HEIGHT]
else:
(width, height) = dimensions
offset_width = 0
offset_height = 0
else:
# Default dimensions, use model defaults and transform if needed
width = model.get_default(CONF_WIDTH)
height = model.get_default(CONF_HEIGHT)
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
# the offset is asymmetric
if transform[CONF_MIRROR_X]:
native_width = model.get_default(
CONF_NATIVE_WIDTH, width + offset_width * 2
)
offset_width = native_width - width - offset_width
if transform[CONF_MIRROR_Y]:
native_height = model.get_default(
CONF_NATIVE_HEIGHT, height + offset_height * 2
)
offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set
if transform[CONF_SWAP_XY] is True:
width, height = height, width
offset_height, offset_width = offset_width, offset_height
color_depth = config[CONF_COLOR_DEPTH]
if color_depth.endswith("bit"):
color_depth = color_depth[:-3]
color_depth = COLOR_DEPTHS[int(color_depth)]
var = cg.new_Pvariable(
config[CONF_ID], width, height, offset_width, offset_height, color_depth
)
cg.add(var.set_init_sequence(get_sequence(model, config))) cg.add(var.set_init_sequence(get_sequence(model, config)))
if rotation_as_transform(model, config): if is_rotation_transformable(config):
if CONF_TRANSFORM in config: if CONF_TRANSFORM in config:
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
else: else:
config[CONF_ROTATION] = 0 config[CONF_ROTATION] = 0
cg.add(var.set_model(config[CONF_MODEL])) cg.add(var.set_model(config[CONF_MODEL]))
cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN]))
cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING])) cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING]))
cg.add(var.set_spi_16(config[CONF_SPI_16]))
if enable_pin := config.get(CONF_ENABLE_PIN): if enable_pin := config.get(CONF_ENABLE_PIN):
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
cg.add(var.set_enable_pins(enable)) cg.add(var.set_enable_pins(enable))
@ -472,4 +621,5 @@ async def to_code(config):
cg.add(var.set_writer(lambda_)) cg.add(var.set_writer(lambda_))
await display.register_display(var, config) await display.register_display(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)
# Displays are write-only, set the SPI device to write-only as well
cg.add(var.set_write_only(True)) cg.add(var.set_write_only(True))

View File

@ -2,489 +2,5 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
namespace mipi_spi { namespace mipi_spi {} // namespace mipi_spi
void MipiSpi::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
this->spi_setup();
if (this->dc_pin_ != nullptr) {
this->dc_pin_->setup();
this->dc_pin_->digital_write(false);
}
for (auto *pin : this->enable_pins_) {
pin->setup();
pin->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
}
this->bus_width_ = this->parent_->get_bus_width();
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
auto when = millis() + 120;
delay(10);
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
ESP_LOGD(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
auto arg_byte = vec[index];
switch (cmd) {
case SLEEP_OUT: {
// are we ready, boots?
int duration = when - millis();
if (duration > 0) {
ESP_LOGD(TAG, "Sleep %dms", duration);
delay(duration);
}
} break;
case INVERT_ON:
this->invert_colors_ = true;
break;
case MADCTL_CMD:
this->madctl_ = arg_byte;
break;
case PIXFMT:
this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18;
break;
case BRIGHTNESS:
this->brightness_ = arg_byte;
break;
default:
break;
}
const auto *ptr = vec.data() + index;
ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
this->write_command_(cmd, ptr, num_args);
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
}
}
this->setup_complete_ = true;
if (this->draw_from_origin_)
check_buffer_();
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
}
void MipiSpi::update() {
if (!this->setup_complete_ || this->is_failed()) {
return;
}
this->do_update_();
if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
return;
ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_);
// Some chips require that the drawing window be aligned on certain boundaries
auto dr = this->draw_rounding_;
this->x_low_ = this->x_low_ / dr * dr;
this->y_low_ = this->y_low_ / dr * dr;
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
if (this->draw_from_origin_) {
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_ - 1;
}
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_,
this->width_ - w - this->x_low_);
// invalidate watermarks
this->x_low_ = this->width_;
this->y_low_ = this->height_;
this->x_high_ = 0;
this->y_high_ = 0;
}
void MipiSpi::fill(Color color) {
if (!this->check_buffer_())
return;
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->get_width_internal() - 1;
this->y_high_ = this->get_height_internal() - 1;
switch (this->color_depth_) {
case display::COLOR_BITNESS_332: {
auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
break;
}
default: {
auto new_color = display::ColorUtil::color_to_565(color);
if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) {
// Upper and lower is equal can use quicker memset operation. Takes ~20ms.
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
} else {
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
auto len = this->buffer_bytes_ / 2;
while (len--) {
*ptr_16++ = new_color;
}
}
}
}
}
void MipiSpi::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
return;
}
if (!this->check_buffer_())
return;
size_t pos = (y * this->width_) + x;
switch (this->color_depth_) {
case display::COLOR_BITNESS_332: {
uint8_t new_color = display::ColorUtil::color_to_332(color);
if (this->buffer_[pos] == new_color)
return;
this->buffer_[pos] = new_color;
break;
}
case display::COLOR_BITNESS_565: {
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5);
uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3);
uint16_t new_color = hi_byte | (lo_byte << 8); // big endian
if (ptr_16[pos] == new_color)
return;
ptr_16[pos] = new_color;
break;
}
default:
return;
}
// low and high watermark may speed up drawing from buffer
if (x < this->x_low_)
this->x_low_ = x;
if (y < this->y_low_)
this->y_low_ = y;
if (x > this->x_high_)
this->x_high_ = x;
if (y > this->y_high_)
this->y_high_ = y;
}
void MipiSpi::reset_params_() {
if (!this->is_ready())
return;
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
if (this->brightness_.has_value())
this->write_command_(BRIGHTNESS, this->brightness_.value());
}
void MipiSpi::write_init_sequence_() {
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
ESP_LOGV(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
const auto *ptr = vec.data() + index;
this->write_command_(cmd, ptr, num_args);
index += num_args;
}
}
this->setup_complete_ = true;
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
}
void MipiSpi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
uint8_t buf[4];
x1 += this->offset_width_;
x2 += this->offset_width_;
y1 += this->offset_height_;
y2 += this->offset_height_;
put16_be(buf, y1);
put16_be(buf + 2, y2);
this->write_command_(RASET, buf, sizeof buf);
put16_be(buf, x1);
put16_be(buf + 2, x2);
this->write_command_(CASET, buf, sizeof buf);
}
void MipiSpi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (!this->setup_complete_ || this->is_failed())
return;
if (w <= 0 || h <= 0)
return;
if (bitness != this->color_depth_ || big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) {
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
return;
}
if (this->draw_from_origin_) {
auto stride = x_offset + w + x_pad;
for (int y = 0; y != h; y++) {
memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2,
ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2);
}
ptr = this->buffer_;
w = this->width_;
h += y_start;
x_start = 0;
y_start = 0;
x_offset = 0;
y_offset = 0;
}
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
}
void MipiSpi::write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride) {
stride -= w;
uint8_t transfer_buffer[6 * 256];
size_t idx = 0; // index into transfer_buffer
while (h-- != 0) {
for (auto x = w; x-- != 0;) {
auto color_val = *ptr++;
// deal with byte swapping
transfer_buffer[idx++] = (color_val & 0xF8); // Blue
transfer_buffer[idx++] = ((color_val & 0x7) << 5) | ((color_val & 0xE000) >> 11); // Green
transfer_buffer[idx++] = (color_val >> 5) & 0xF8; // Red
if (idx == sizeof(transfer_buffer)) {
this->write_array(transfer_buffer, idx);
idx = 0;
}
}
ptr += stride;
}
if (idx != 0)
this->write_array(transfer_buffer, idx);
}
void MipiSpi::write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
stride -= w;
uint8_t transfer_buffer[6 * 256];
size_t idx = 0; // index into transfer_buffer
while (h-- != 0) {
for (auto x = w; x-- != 0;) {
auto color_val = *ptr++;
transfer_buffer[idx++] = color_val & 0xE0; // Red
transfer_buffer[idx++] = (color_val << 3) & 0xE0; // Green
transfer_buffer[idx++] = color_val << 6; // Blue
if (idx == sizeof(transfer_buffer)) {
this->write_array(transfer_buffer, idx);
idx = 0;
}
}
ptr += stride;
}
if (idx != 0)
this->write_array(transfer_buffer, idx);
}
void MipiSpi::write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
stride -= w;
uint8_t transfer_buffer[6 * 256];
size_t idx = 0; // index into transfer_buffer
while (h-- != 0) {
for (auto x = w; x-- != 0;) {
auto color_val = *ptr++;
transfer_buffer[idx++] = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
transfer_buffer[idx++] = (color_val & 0x3) << 3;
if (idx == sizeof(transfer_buffer)) {
this->write_array(transfer_buffer, idx);
idx = 0;
}
}
ptr += stride;
}
if (idx != 0)
this->write_array(transfer_buffer, idx);
}
void MipiSpi::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad) {
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
auto stride = x_offset + w + x_pad;
const auto *offset_ptr = ptr;
if (this->color_depth_ == display::COLOR_BITNESS_332) {
offset_ptr += y_offset * stride + x_offset;
} else {
stride *= 2;
offset_ptr += y_offset * stride + x_offset * 2;
}
switch (this->bus_width_) {
case 4:
this->enable();
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't
// bother
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h * 2, 4);
} else {
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, nullptr, 0, 4);
for (int y = 0; y != h; y++) {
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 4);
offset_ptr += stride;
}
}
break;
case 8:
this->write_command_(WDATA);
this->enable();
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h * 2, 8);
} else {
for (int y = 0; y != h; y++) {
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 8);
offset_ptr += stride;
}
}
break;
default:
this->write_command_(WDATA);
this->enable();
if (this->color_depth_ == display::COLOR_BITNESS_565) {
// Source buffer is 16-bit RGB565
if (this->pixel_mode_ == PIXEL_MODE_18) {
// Convert RGB565 to RGB666
this->write_18_from_16_bit_(reinterpret_cast<const uint16_t *>(offset_ptr), w, h, stride / 2);
} else {
// Direct RGB565 output
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
this->write_array(ptr, w * h * 2);
} else {
for (int y = 0; y != h; y++) {
this->write_array(offset_ptr, w * 2);
offset_ptr += stride;
}
}
}
} else {
// Source buffer is 8-bit RGB332
if (this->pixel_mode_ == PIXEL_MODE_18) {
// Convert RGB332 to RGB666
this->write_18_from_8_bit_(offset_ptr, w, h, stride);
} else {
this->write_16_from_8_bit_(offset_ptr, w, h, stride);
}
break;
}
}
this->disable();
}
void MipiSpi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
if (this->bus_width_ == 4) {
this->enable();
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
this->disable();
} else if (this->bus_width_ == 8) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
this->disable();
}
} else {
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
if (this->spi_16_) {
for (size_t i = 0; i != len; i++) {
this->enable();
this->write_byte(0);
this->write_byte(bytes[i]);
this->disable();
}
} else {
this->enable();
this->write_array(bytes, len);
this->disable();
}
}
}
}
void MipiSpi::dump_config() {
ESP_LOGCONFIG(TAG,
"MIPI_SPI Display\n"
" Model: %s\n"
" Width: %u\n"
" Height: %u",
this->model_, this->width_, this->height_);
if (this->offset_width_ != 0)
ESP_LOGCONFIG(TAG, " Offset width: %u", this->offset_width_);
if (this->offset_height_ != 0)
ESP_LOGCONFIG(TAG, " Offset height: %u", this->offset_height_);
ESP_LOGCONFIG(TAG,
" Swap X/Y: %s\n"
" Mirror X: %s\n"
" Mirror Y: %s\n"
" Color depth: %d bits\n"
" Invert colors: %s\n"
" Color order: %s\n"
" Pixel mode: %s",
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)),
this->color_depth_ == display::COLOR_BITNESS_565 ? 16 : 8, YESNO(this->invert_colors_),
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", this->pixel_mode_ == PIXEL_MODE_18 ? "18bit" : "16bit");
if (this->brightness_.has_value())
ESP_LOGCONFIG(TAG, " Brightness: %u", this->brightness_.value());
if (this->spi_16_)
ESP_LOGCONFIG(TAG, " SPI 16bit: YES");
ESP_LOGCONFIG(TAG, " Draw rounding: %u", this->draw_rounding_);
if (this->draw_from_origin_)
ESP_LOGCONFIG(TAG, " Draw from origin: YES");
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
ESP_LOGCONFIG(TAG,
" SPI Mode: %d\n"
" SPI Data rate: %dMHz\n"
" SPI Bus width: %d",
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), this->bus_width_);
}
} // namespace mipi_spi
} // namespace esphome } // namespace esphome

View File

@ -4,40 +4,39 @@
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h" #include "esphome/components/display/display.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/display_color_utils.h" #include "esphome/components/display/display_color_utils.h"
namespace esphome { namespace esphome {
namespace mipi_spi { namespace mipi_spi {
constexpr static const char *const TAG = "display.mipi_spi"; constexpr static const char *const TAG = "display.mipi_spi";
static const uint8_t SW_RESET_CMD = 0x01; static constexpr uint8_t SW_RESET_CMD = 0x01;
static const uint8_t SLEEP_OUT = 0x11; static constexpr uint8_t SLEEP_OUT = 0x11;
static const uint8_t NORON = 0x13; static constexpr uint8_t NORON = 0x13;
static const uint8_t INVERT_OFF = 0x20; static constexpr uint8_t INVERT_OFF = 0x20;
static const uint8_t INVERT_ON = 0x21; static constexpr uint8_t INVERT_ON = 0x21;
static const uint8_t ALL_ON = 0x23; static constexpr uint8_t ALL_ON = 0x23;
static const uint8_t WRAM = 0x24; static constexpr uint8_t WRAM = 0x24;
static const uint8_t MIPI = 0x26; static constexpr uint8_t MIPI = 0x26;
static const uint8_t DISPLAY_ON = 0x29; static constexpr uint8_t DISPLAY_ON = 0x29;
static const uint8_t RASET = 0x2B; static constexpr uint8_t RASET = 0x2B;
static const uint8_t CASET = 0x2A; static constexpr uint8_t CASET = 0x2A;
static const uint8_t WDATA = 0x2C; static constexpr uint8_t WDATA = 0x2C;
static const uint8_t TEON = 0x35; static constexpr uint8_t TEON = 0x35;
static const uint8_t MADCTL_CMD = 0x36; static constexpr uint8_t MADCTL_CMD = 0x36;
static const uint8_t PIXFMT = 0x3A; static constexpr uint8_t PIXFMT = 0x3A;
static const uint8_t BRIGHTNESS = 0x51; static constexpr uint8_t BRIGHTNESS = 0x51;
static const uint8_t SWIRE1 = 0x5A; static constexpr uint8_t SWIRE1 = 0x5A;
static const uint8_t SWIRE2 = 0x5B; static constexpr uint8_t SWIRE2 = 0x5B;
static const uint8_t PAGESEL = 0xFE; static constexpr uint8_t PAGESEL = 0xFE;
static const uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
static const uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
static const uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
static const uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
static const uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
static const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
static const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static const uint8_t DELAY_FLAG = 0xFF; static const uint8_t DELAY_FLAG = 0xFF;
// store a 16 bit value in a buffer, big endian. // store a 16 bit value in a buffer, big endian.
@ -46,28 +45,44 @@ static inline void put16_be(uint8_t *buf, uint16_t value) {
buf[1] = value; buf[1] = value;
} }
// Buffer mode, conveniently also the number of bytes in a pixel
enum PixelMode { enum PixelMode {
PIXEL_MODE_16, PIXEL_MODE_8 = 1,
PIXEL_MODE_18, PIXEL_MODE_16 = 2,
PIXEL_MODE_18 = 3,
}; };
class MipiSpi : public display::DisplayBuffer, enum BusType {
BUS_TYPE_SINGLE = 1,
BUS_TYPE_QUAD = 4,
BUS_TYPE_OCTAL = 8,
BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer
};
/**
* Base class for MIPI SPI displays.
* All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file.
*
* @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t
* @tparam BUFFERPIXEL Color depth of the buffer
* @tparam DISPLAYPIXEL Color depth of the display
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
* @tparam WIDTH Width of the display in pixels
* @tparam HEIGHT Height of the display in pixels
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
* buffer
*/
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT>
class MipiSpi : public display::Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_1MHZ> { spi::DATA_RATE_1MHZ> {
public: public:
MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth) MipiSpi() {}
: width_(width), void update() override { this->stop_poller(); }
height_(height), void draw_pixel_at(int x, int y, Color color) override {}
offset_width_(offset_width),
offset_height_(offset_height),
color_depth_(color_depth) {}
void set_model(const char *model) { this->model_ = model; } void set_model(const char *model) { this->model_ = model; }
void update() override;
void setup() override;
display::ColorOrder get_color_mode() {
return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB;
}
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); } void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; }
@ -79,93 +94,524 @@ class MipiSpi : public display::DisplayBuffer,
this->brightness_ = brightness; this->brightness_ = brightness;
this->reset_params_(); this->reset_params_();
} }
void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; }
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
void dump_config() override;
int get_width_internal() override { return this->width_; } int get_width_internal() override { return WIDTH; }
int get_height_internal() override { return this->height_; } int get_height_internal() override { return HEIGHT; }
bool can_proceed() override { return this->setup_complete_; }
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; } void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; } void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; }
// reset the display, and write the init sequence
void setup() override {
this->spi_setup();
if (this->dc_pin_ != nullptr) {
this->dc_pin_->setup();
this->dc_pin_->digital_write(false);
}
for (auto *pin : this->enable_pins_) {
pin->setup();
pin->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
}
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
auto when = millis() + 120;
delay(10);
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
esph_log_e(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
esph_log_d(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
esph_log_e(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
auto arg_byte = vec[index];
switch (cmd) {
case SLEEP_OUT: {
// are we ready, boots?
int duration = when - millis();
if (duration > 0) {
esph_log_d(TAG, "Sleep %dms", duration);
delay(duration);
}
} break;
case INVERT_ON:
this->invert_colors_ = true;
break;
case MADCTL_CMD:
this->madctl_ = arg_byte;
break;
case BRIGHTNESS:
this->brightness_ = arg_byte;
break;
default:
break;
}
const auto *ptr = vec.data() + index;
esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
this->write_command_(cmd, ptr, num_args);
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
}
}
// init sequence no longer needed
this->init_sequence_.clear();
}
// Drawing operations
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override {
if (this->is_failed())
return;
if (w <= 0 || h <= 0)
return;
if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN) {
// note that the usual logging macros are banned in header files, so use their replacement
esph_log_e(TAG, "Unsupported color depth or bit order");
return;
}
this->write_to_display_(x_start, y_start, w, h, reinterpret_cast<const BUFFERTYPE *>(ptr), x_offset, y_offset,
x_pad);
}
void dump_config() override {
esph_log_config(TAG,
"MIPI_SPI Display\n"
" Model: %s\n"
" Width: %u\n"
" Height: %u",
this->model_, WIDTH, HEIGHT);
if constexpr (OFFSET_WIDTH != 0)
esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH);
if constexpr (OFFSET_HEIGHT != 0)
esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT);
esph_log_config(TAG,
" Swap X/Y: %s\n"
" Mirror X: %s\n"
" Mirror Y: %s\n"
" Invert colors: %s\n"
" Color order: %s\n"
" Display pixels: %d bits\n"
" Endianness: %s\n",
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_),
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little");
if (this->brightness_.has_value())
esph_log_config(TAG, " Brightness: %u", this->brightness_.value());
if (this->cs_ != nullptr)
esph_log_config(TAG, " CS Pin: %s", this->cs_->dump_summary().c_str());
if (this->reset_pin_ != nullptr)
esph_log_config(TAG, " Reset Pin: %s", this->reset_pin_->dump_summary().c_str());
if (this->dc_pin_ != nullptr)
esph_log_config(TAG, " DC Pin: %s", this->dc_pin_->dump_summary().c_str());
esph_log_config(TAG,
" SPI Mode: %d\n"
" SPI Data rate: %dMHz\n"
" SPI Bus width: %d",
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), BUS_TYPE);
}
protected: protected:
bool check_buffer_() { /* METHODS */
if (this->is_failed()) // convenience functions to write commands with or without data
return false;
if (this->buffer_ != nullptr)
return true;
auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1;
this->init_internal_(this->width_ * this->height_ * bytes_per_pixel);
if (this->buffer_ == nullptr) {
this->mark_failed();
return false;
}
this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel;
return true;
}
void fill(Color color) override;
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
void write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride);
void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad);
/**
* the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the
* sample code.)
*
* Immediately after enabling /CS send 4 bytes in single-dataline SPI mode:
* 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be
* sent in 1-dataline SPI. The second indicates quad mode.
* 1: 0x00
* 2: The command (register address) byte.
* 3: 0x00
*
* This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte.
* At the conclusion of the write, de-assert /CS.
*
* @param cmd
* @param bytes
* @param len
*/
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len);
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); } void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); } void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
void reset_params_();
void write_init_sequence_();
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
// Writes a command to the display, with the given bytes.
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->enable();
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
this->disable();
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
this->disable();
}
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
this->enable();
this->write_array(bytes, len);
this->disable();
}
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->disable();
this->dc_pin_->digital_write(true);
for (size_t i = 0; i != len; i++) {
this->enable();
this->write_byte(0);
this->write_byte(bytes[i]);
this->disable();
}
}
}
// write changed parameters to the display
void reset_params_() {
if (!this->is_ready())
return;
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
if (this->brightness_.has_value())
this->write_command_(BRIGHTNESS, this->brightness_.value());
}
// set the address window for the next data write
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
uint8_t buf[4];
x1 += OFFSET_WIDTH;
x2 += OFFSET_WIDTH;
y1 += OFFSET_HEIGHT;
y2 += OFFSET_HEIGHT;
put16_be(buf, y1);
put16_be(buf + 2, y2);
this->write_command_(RASET, buf, sizeof buf);
put16_be(buf, x1);
put16_be(buf + 2, x2);
this->write_command_(CASET, buf, sizeof buf);
if constexpr (BUS_TYPE != BUS_TYPE_QUAD) {
this->write_command_(WDATA);
}
}
// map the display color bitness to the pixel mode
static PixelMode get_pixel_mode(display::ColorBitness bitness) {
switch (bitness) {
case display::COLOR_BITNESS_888:
return PIXEL_MODE_18; // 18 bits per pixel
case display::COLOR_BITNESS_565:
return PIXEL_MODE_16; // 16 bits per pixel
default:
return PIXEL_MODE_8; // Default to 8 bits per pixel
}
}
/**
* Writes a buffer to the display.
* @param w Width of each line in bytes
* @param h Height of the buffer in rows
* @param pad Padding in bytes after each line
*/
void write_display_data_(const uint8_t *ptr, size_t w, size_t h, size_t pad) {
if (pad == 0) {
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->write_array(ptr, w * h);
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h, 4);
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8);
}
} else {
for (size_t y = 0; y != h; y++) {
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->write_array(ptr, w);
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w, 4);
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w, 8);
}
ptr += w + pad;
}
}
}
/**
* Writes a buffer to the display.
*
* The ptr is a pointer to the pixel data
* The other parameters are all in pixel units.
*/
void write_to_display_(int x_start, int y_start, int w, int h, const BUFFERTYPE *ptr, int x_offset, int y_offset,
int x_pad) {
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
this->enable();
ptr += y_offset * (x_offset + w + x_pad) + x_offset;
if constexpr (BUFFERPIXEL == DISPLAYPIXEL) {
this->write_display_data_(reinterpret_cast<const uint8_t *>(ptr), w * sizeof(BUFFERTYPE), h,
x_pad * sizeof(BUFFERTYPE));
} else {
// type conversion required, do it in chunks
uint8_t dbuffer[DISPLAYPIXEL * 48];
uint8_t *dptr = dbuffer;
auto stride = x_offset + w + x_pad; // stride in pixels
for (size_t y = 0; y != h; y++) {
for (size_t x = 0; x != w; x++) {
auto color_val = ptr[y * stride + x];
if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_16) {
// 16 to 18 bit conversion
if constexpr (IS_BIG_ENDIAN) {
*dptr++ = color_val & 0xF8;
*dptr++ = ((color_val & 0x7) << 5) | (color_val & 0xE000) >> 11;
*dptr++ = (color_val >> 5) & 0xF8;
} else {
*dptr++ = (color_val >> 8) & 0xF8; // Blue
*dptr++ = (color_val & 0x7E0) >> 3;
*dptr++ = color_val << 3;
}
} else if constexpr (DISPLAYPIXEL == PIXEL_MODE_18 && BUFFERPIXEL == PIXEL_MODE_8) {
// 8 bit to 18 bit conversion
*dptr++ = color_val << 6; // Blue
*dptr++ = (color_val & 0x1C) << 3; // Green
*dptr++ = (color_val & 0xE0); // Red
} else if constexpr (DISPLAYPIXEL == PIXEL_MODE_16 && BUFFERPIXEL == PIXEL_MODE_8) {
if constexpr (IS_BIG_ENDIAN) {
*dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
*dptr++ = (color_val & 3) << 3;
} else {
*dptr++ = (color_val & 3) << 3;
*dptr++ = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
}
}
// buffer full? Flush.
if (dptr == dbuffer + sizeof(dbuffer)) {
this->write_display_data_(dbuffer, sizeof(dbuffer), 1, 0);
dptr = dbuffer;
}
}
}
// flush any remaining data
if (dptr != dbuffer) {
this->write_display_data_(dbuffer, dptr - dbuffer, 1, 0);
}
}
this->disable();
}
/* PROPERTIES */
// GPIO pins
GPIOPin *reset_pin_{nullptr}; GPIOPin *reset_pin_{nullptr};
std::vector<GPIOPin *> enable_pins_{}; std::vector<GPIOPin *> enable_pins_{};
GPIOPin *dc_pin_{nullptr}; GPIOPin *dc_pin_{nullptr};
uint16_t x_low_{1};
uint16_t y_low_{1};
uint16_t x_high_{0};
uint16_t y_high_{0};
bool setup_complete_{};
// other properties set by configuration
bool invert_colors_{}; bool invert_colors_{};
size_t width_;
size_t height_;
int16_t offset_width_;
int16_t offset_height_;
size_t buffer_bytes_{0};
display::ColorBitness color_depth_;
PixelMode pixel_mode_{PIXEL_MODE_16};
uint8_t bus_width_{};
bool spi_16_{};
uint8_t madctl_{};
bool draw_from_origin_{false};
unsigned draw_rounding_{2}; unsigned draw_rounding_{2};
optional<uint8_t> brightness_{}; optional<uint8_t> brightness_{};
const char *model_{"Unknown"}; const char *model_{"Unknown"};
std::vector<uint8_t> init_sequence_{}; std::vector<uint8_t> init_sequence_{};
uint8_t madctl_{};
}; };
/**
* Class for MIPI SPI displays with a buffer.
*
* @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t
* @tparam BUFFERPIXEL Color depth of the buffer
* @tparam DISPLAYPIXEL Color depth of the display
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
* @tparam ROTATION The rotation of the display
* @tparam WIDTH Width of the display in pixels
* @tparam HEIGHT Height of the display in pixels
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
* @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer).
*/
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION, int FRACTION>
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
OFFSET_WIDTH, OFFSET_HEIGHT> {
public:
MipiSpiBuffer() { this->rotation_ = ROTATION; }
void dump_config() override {
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
OFFSET_HEIGHT>::dump_config();
esph_log_config(TAG,
" Rotation: %d°\n"
" Buffer pixels: %d bits\n"
" Buffer fraction: 1/%d\n"
" Buffer bytes: %zu\n"
" Draw rounding: %u",
this->rotation_, BUFFERPIXEL * 8, FRACTION, sizeof(BUFFERTYPE) * WIDTH * HEIGHT / FRACTION,
this->draw_rounding_);
}
void setup() override {
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
OFFSET_HEIGHT>::setup();
RAMAllocator<BUFFERTYPE> allocator{};
this->buffer_ = allocator.allocate(WIDTH * HEIGHT / FRACTION);
if (this->buffer_ == nullptr) {
this->mark_failed("Buffer allocation failed");
}
}
void update() override {
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
auto now = millis();
#endif
if (this->is_failed()) {
return;
}
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
// the display height,
for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) {
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
auto lap = millis();
#endif
this->end_line_ = this->start_line_ + HEIGHT / FRACTION;
if (this->auto_clear_enabled_) {
this->clear();
}
if (this->page_ != nullptr) {
this->page_->get_writer()(*this);
} else if (this->writer_.has_value()) {
(*this->writer_)(*this);
} else {
this->test_card();
}
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
esph_log_v(TAG, "Drawing from line %d took %dms", this->start_line_, millis() - lap);
lap = millis();
#endif
if (this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
return;
esph_log_v(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_,
this->y_high_);
// Some chips require that the drawing window be aligned on certain boundaries
auto dr = this->draw_rounding_;
this->x_low_ = this->x_low_ / dr * dr;
this->y_low_ = this->y_low_ / dr * dr;
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
this->y_low_ - this->start_line_, WIDTH - w);
// invalidate watermarks
this->x_low_ = WIDTH;
this->y_low_ = HEIGHT;
this->x_high_ = 0;
this->y_high_ = 0;
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
esph_log_v(TAG, "Write to display took %dms", millis() - lap);
lap = millis();
#endif
}
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
esph_log_v(TAG, "Total update took %dms", millis() - now);
#endif
}
// Draw a pixel at the given coordinates.
void draw_pixel_at(int x, int y, Color color) override {
rotate_coordinates_(x, y);
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
return;
this->buffer_[(y - this->start_line_) * WIDTH + x] = convert_color_(color);
if (x < this->x_low_) {
this->x_low_ = x;
}
if (x > this->x_high_) {
this->x_high_ = x;
}
if (y < this->y_low_) {
this->y_low_ = y;
}
if (y > this->y_high_) {
this->y_high_ = y;
}
}
// Fills the display with a color.
void fill(Color color) override {
this->x_low_ = 0;
this->y_low_ = this->start_line_;
this->x_high_ = WIDTH - 1;
this->y_high_ = this->end_line_ - 1;
std::fill_n(this->buffer_, HEIGHT * WIDTH / FRACTION, convert_color_(color));
}
int get_width() override {
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
return HEIGHT;
return WIDTH;
}
int get_height() override {
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
return WIDTH;
return HEIGHT;
}
protected:
// Rotate the coordinates to match the display orientation.
void rotate_coordinates_(int &x, int &y) const {
if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) {
x = WIDTH - x - 1;
y = HEIGHT - y - 1;
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) {
auto tmp = x;
x = WIDTH - y - 1;
y = tmp;
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) {
auto tmp = y;
y = HEIGHT - x - 1;
x = tmp;
}
}
// Convert a color to the buffer pixel format.
BUFFERTYPE convert_color_(Color &color) const {
if constexpr (BUFFERPIXEL == PIXEL_MODE_8) {
return (color.red & 0xE0) | (color.g & 0xE0) >> 3 | color.b >> 6;
} else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) {
if constexpr (IS_BIG_ENDIAN) {
return (color.r & 0xF8) | color.g >> 5 | (color.g & 0x1C) << 11 | (color.b & 0xF8) << 5;
} else {
return (color.r & 0xF8) << 8 | (color.g & 0xFC) << 3 | color.b >> 3;
}
}
return static_cast<BUFFERTYPE>(0);
}
BUFFERTYPE *buffer_{};
uint16_t x_low_{WIDTH};
uint16_t y_low_{HEIGHT};
uint16_t x_high_{0};
uint16_t y_high_{0};
uint16_t start_line_{0};
uint16_t end_line_{1};
};
} // namespace mipi_spi } // namespace mipi_spi
} // namespace esphome } // namespace esphome

View File

@ -0,0 +1,30 @@
from .ili import ST7789V
ST7789V.extend(
"ADAFRUIT-FUNHOUSE",
height=240,
width=240,
offset_height=0,
offset_width=0,
cs_pin=40,
dc_pin=39,
reset_pin=41,
invert_colors=True,
mirror_x=True,
mirror_y=True,
data_rate="80MHz",
)
ST7789V.extend(
"ADAFRUIT-S2-TFT-FEATHER",
height=240,
width=135,
offset_height=52,
offset_width=40,
cs_pin=7,
dc_pin=39,
reset_pin=40,
invert_colors=True,
)
models = {}

View File

@ -67,6 +67,14 @@ RM690B0 = DriverChip(
), ),
) )
T4_S3_AMOLED = RM690B0.extend("T4-S3", width=450, offset_width=16, bus_mode=TYPE_QUAD) T4_S3_AMOLED = RM690B0.extend(
"T4-S3",
width=450,
offset_width=16,
cs_pin=11,
reset_pin=13,
enable_pin=9,
bus_mode=TYPE_QUAD,
)
models = {} models = {}

View File

@ -1,3 +1,5 @@
import esphome.config_validation as cv
from . import DriverChip from . import DriverChip
from .ili import ILI9488_A from .ili import ILI9488_A
@ -128,6 +130,7 @@ DriverChip(
ILI9488_A.extend( ILI9488_A.extend(
"PICO-RESTOUCH-LCD-3.5", "PICO-RESTOUCH-LCD-3.5",
swap_xy=cv.UNDEFINED,
spi_16=True, spi_16=True,
pixel_mode="16bit", pixel_mode="16bit",
mirror_x=True, mirror_x=True,

View File

@ -193,13 +193,17 @@ void MQTTClientComponent::start_dnslookup_() {
this->dns_resolve_error_ = false; this->dns_resolve_error_ = false;
this->dns_resolved_ = false; this->dns_resolved_ = false;
ip_addr_t addr; ip_addr_t addr;
err_t err;
{
LwIPLock lock;
#if USE_NETWORK_IPV6 #if USE_NETWORK_IPV6
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV6_IPV4); this, LWIP_DNS_ADDRTYPE_IPV6_IPV4);
#else #else
err_t err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, err = dns_gethostbyname_addrtype(this->credentials_.address.c_str(), &addr, MQTTClientComponent::dns_found_callback,
MQTTClientComponent::dns_found_callback, this, LWIP_DNS_ADDRTYPE_IPV4); this, LWIP_DNS_ADDRTYPE_IPV4);
#endif /* USE_NETWORK_IPV6 */ #endif /* USE_NETWORK_IPV6 */
}
switch (err) { switch (err) {
case ERR_OK: { case ERR_OK: {
// Got IP immediately // Got IP immediately

View File

@ -129,21 +129,16 @@ bool MQTTComponent::send_discovery_() {
root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available; root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available;
} }
std::string unique_id = this->unique_id();
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
if (!unique_id.empty()) { if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
root[MQTT_UNIQUE_ID] = unique_id; char friendly_name_hash[9];
sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name()));
friendly_name_hash[8] = 0; // ensure the hash-string ends with null
root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash;
} else { } else {
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { // default to almost-unique ID. It's a hack but the only way to get that
char friendly_name_hash[9]; // gorgeous device registry view.
sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name())); root[MQTT_UNIQUE_ID] = "ESP" + this->component_type() + this->get_default_object_id_();
friendly_name_hash[8] = 0; // ensure the hash-string ends with null
root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash;
} else {
// default to almost-unique ID. It's a hack but the only way to get that
// gorgeous device registry view.
root[MQTT_UNIQUE_ID] = "ESP" + this->component_type() + this->get_default_object_id_();
}
} }
const std::string &node_name = App.get_name(); const std::string &node_name = App.get_name();
@ -286,7 +281,6 @@ void MQTTComponent::call_dump_config() {
this->dump_config(); this->dump_config();
} }
void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; }
std::string MQTTComponent::unique_id() { return ""; }
bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); }
// Pull these properties from EntityBase if not overridden // Pull these properties from EntityBase if not overridden

View File

@ -164,13 +164,6 @@ class MQTTComponent : public Component {
*/ */
virtual const EntityBase *get_entity() const = 0; virtual const EntityBase *get_entity() const = 0;
/** A unique ID for this MQTT component, empty for no unique id. See unique ID requirements:
* https://developers.home-assistant.io/docs/en/entity_registry_index.html#unique-id-requirements
*
* @return The unique id as a string.
*/
virtual std::string unique_id();
/// Get the friendly name of this MQTT component. /// Get the friendly name of this MQTT component.
virtual std::string friendly_name() const; virtual std::string friendly_name() const;

View File

@ -76,7 +76,6 @@ bool MQTTSensorComponent::publish_state(float value) {
int8_t accuracy = this->sensor_->get_accuracy_decimals(); int8_t accuracy = this->sensor_->get_accuracy_decimals();
return this->publish(this->get_state_topic_(), value_accuracy_to_string(value, accuracy)); return this->publish(this->get_state_topic_(), value_accuracy_to_string(value, accuracy));
} }
std::string MQTTSensorComponent::unique_id() { return this->sensor_->unique_id(); }
} // namespace mqtt } // namespace mqtt
} // namespace esphome } // namespace esphome

View File

@ -46,7 +46,6 @@ class MQTTSensorComponent : public mqtt::MQTTComponent {
/// Override for MQTTComponent, returns "sensor". /// Override for MQTTComponent, returns "sensor".
std::string component_type() const override; std::string component_type() const override;
const EntityBase *get_entity() const override; const EntityBase *get_entity() const override;
std::string unique_id() override;
sensor::Sensor *sensor_; sensor::Sensor *sensor_;
optional<uint32_t> expire_after_; // Override the expire after advertised to Home Assistant optional<uint32_t> expire_after_; // Override the expire after advertised to Home Assistant

View File

@ -40,7 +40,6 @@ bool MQTTTextSensor::send_initial_state() {
} }
std::string MQTTTextSensor::component_type() const { return "sensor"; } std::string MQTTTextSensor::component_type() const { return "sensor"; }
const EntityBase *MQTTTextSensor::get_entity() const { return this->sensor_; } const EntityBase *MQTTTextSensor::get_entity() const { return this->sensor_; }
std::string MQTTTextSensor::unique_id() { return this->sensor_->unique_id(); }
} // namespace mqtt } // namespace mqtt
} // namespace esphome } // namespace esphome

View File

@ -28,7 +28,6 @@ class MQTTTextSensor : public mqtt::MQTTComponent {
protected: protected:
std::string component_type() const override; std::string component_type() const override;
const EntityBase *get_entity() const override; const EntityBase *get_entity() const override;
std::string unique_id() override;
text_sensor::TextSensor *sensor_; text_sensor::TextSensor *sensor_;
}; };

View File

@ -0,0 +1,218 @@
from __future__ import annotations
from pathlib import Path
import esphome.codegen as cg
from esphome.components.zephyr import (
copy_files as zephyr_copy_files,
zephyr_add_pm_static,
zephyr_set_core_data,
zephyr_to_code,
)
from esphome.components.zephyr.const import (
BOOTLOADER_MCUBOOT,
KEY_BOOTLOADER,
KEY_ZEPHYR,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_BOARD,
CONF_FRAMEWORK,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_NRF52,
)
from esphome.core import CORE, EsphomeError, coroutine_with_priority
from esphome.storage_json import StorageJSON
from esphome.types import ConfigType
from .boards import BOARDS_ZEPHYR, BOOTLOADER_CONFIG
from .const import (
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
)
# force import gpio to register pin schema
from .gpio import nrf52_pin_to_code # noqa
CODEOWNERS = ["@tomaszduda23"]
AUTO_LOAD = ["zephyr"]
IS_TARGET_PLATFORM = True
def set_core_data(config: ConfigType) -> ConfigType:
zephyr_set_core_data(config)
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(2, 6, 1)
if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG:
zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]])
return config
BOOTLOADERS = [
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
BOOTLOADER_MCUBOOT,
]
def _detect_bootloader(config: ConfigType) -> ConfigType:
"""Detect the bootloader for the given board."""
config = config.copy()
bootloaders: list[str] = []
board = config[CONF_BOARD]
if board in BOARDS_ZEPHYR and KEY_BOOTLOADER in BOARDS_ZEPHYR[board]:
# this board have bootloaders config available
bootloaders = BOARDS_ZEPHYR[board][KEY_BOOTLOADER]
if KEY_BOOTLOADER not in config:
if bootloaders:
# there is no bootloader in config -> take first one
config[KEY_BOOTLOADER] = bootloaders[0]
else:
# make mcuboot as default if there is no configuration for that board
config[KEY_BOOTLOADER] = BOOTLOADER_MCUBOOT
elif bootloaders and config[KEY_BOOTLOADER] not in bootloaders:
raise cv.Invalid(
f"{board} does not support {config[KEY_BOOTLOADER]}, select one of: {', '.join(bootloaders)}"
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_BOARD): cv.string_strict,
cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True),
}
),
_detect_bootloader,
set_core_data,
)
@coroutine_with_priority(1000)
async def to_code(config: ConfigType) -> None:
"""Convert the configuration to code."""
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_NRF52")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "NRF52")
cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK])
cg.add_platformio_option(
"platform",
"https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip",
)
cg.add_platformio_option(
"platform_packages",
[
"platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip",
"platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.16.1-1.zip",
],
)
if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT:
# make sure that firmware.zip is created
# for Adafruit_nRF52_Bootloader
cg.add_platformio_option("board_upload.protocol", "nrfutil")
cg.add_platformio_option("board_upload.use_1200bps_touch", "true")
cg.add_platformio_option("board_upload.require_upload_port", "true")
cg.add_platformio_option("board_upload.wait_for_upload_port", "true")
zephyr_to_code(config)
def copy_files() -> None:
"""Copy files to the build directory."""
zephyr_copy_files()
def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]:
"""Get the download types for the firmware."""
types = []
UF2_PATH = "zephyr/zephyr.uf2"
DFU_PATH = "firmware.zip"
HEX_PATH = "zephyr/zephyr.hex"
HEX_MERGED_PATH = "zephyr/merged.hex"
APP_IMAGE_PATH = "zephyr/app_update.bin"
build_dir = Path(storage_json.firmware_bin_path).parent
if (build_dir / UF2_PATH).is_file():
types = [
{
"title": "UF2 package (recommended)",
"description": "For flashing via Adafruit nRF52 Bootloader as a flash drive.",
"file": UF2_PATH,
"download": f"{storage_json.name}.uf2",
},
{
"title": "DFU package",
"description": "For flashing via adafruit-nrfutil using USB CDC.",
"file": DFU_PATH,
"download": f"dfu-{storage_json.name}.zip",
},
]
else:
types = [
{
"title": "HEX package",
"description": "For flashing via pyocd using SWD.",
"file": (
HEX_MERGED_PATH
if (build_dir / HEX_MERGED_PATH).is_file()
else HEX_PATH
),
"download": f"{storage_json.name}.hex",
},
]
if (build_dir / APP_IMAGE_PATH).is_file():
types += [
{
"title": "App update package",
"description": "For flashing via mcumgr-web using BLE or smpclient using USB CDC.",
"file": APP_IMAGE_PATH,
"download": f"app-{storage_json.name}.img",
},
]
return types
def _upload_using_platformio(
config: ConfigType, port: str, upload_args: list[str]
) -> int | str:
from esphome import platformio_api
if port is not None:
upload_args += ["--upload-port", port]
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
def upload_program(config: ConfigType, args, host: str) -> bool:
from esphome.__main__ import check_permissions, get_port_type
result = 0
handled = False
if get_port_type(host) == "SERIAL":
check_permissions(host)
result = _upload_using_platformio(config, host, ["-t", "upload"])
handled = True
if host == "PYOCD":
result = _upload_using_platformio(config, host, ["-t", "flash_pyocd"])
handled = True
if result != 0:
raise EsphomeError(f"Upload failed with result: {result}")
return handled

View File

@ -0,0 +1,34 @@
from esphome.components.zephyr import Section
from esphome.components.zephyr.const import KEY_BOOTLOADER
from .const import (
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
)
BOARDS_ZEPHYR = {
"adafruit_itsybitsy_nrf52840": {
KEY_BOOTLOADER: [
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
]
},
}
# https://github.com/ffenix113/zigbee_home/blob/17bb7b9e9d375e756da9e38913f53303937fb66a/types/board/known_boards.go
# https://learn.adafruit.com/introducing-the-adafruit-nrf52840-feather?view=all#hathach-memory-map
BOOTLOADER_CONFIG = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: [
Section("empty_app_offset", 0x0, 0x26000, "flash_primary"),
],
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: [
Section("empty_app_offset", 0x0, 0x26000, "flash_primary"),
],
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: [
Section("empty_app_offset", 0x0, 0x27000, "flash_primary"),
],
}

View File

@ -0,0 +1,4 @@
BOOTLOADER_ADAFRUIT = "adafruit"
BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132"
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6"
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7"

View File

@ -0,0 +1,53 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components.zephyr.const import zephyr_ns
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INVERTED, CONF_MODE, CONF_NUMBER, PLATFORM_NRF52
ZephyrGPIOPin = zephyr_ns.class_("ZephyrGPIOPin", cg.InternalGPIOPin)
def _translate_pin(value):
if isinstance(value, dict) or value is None:
raise cv.Invalid(
"This variable only supports pin numbers, not full pin schemas "
"(with inverted and mode)."
)
if isinstance(value, int):
return value
try:
return int(value)
except ValueError:
pass
# e.g. P0.27
if len(value) >= len("P0.0") and value[0] == "P" and value[2] == ".":
return cv.int_(value[len("P")].strip()) * 32 + cv.int_(
value[len("P0.") :].strip()
)
raise cv.Invalid(f"Invalid pin: {value}")
def validate_gpio_pin(value):
value = _translate_pin(value)
if value < 0 or value > (32 + 16):
raise cv.Invalid(f"NRF52: Invalid pin number: {value}")
return value
NRF52_PIN_SCHEMA = cv.All(
pins.gpio_base_schema(
ZephyrGPIOPin,
validate_gpio_pin,
modes=pins.GPIO_STANDARD_MODES,
),
)
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_NRF52, NRF52_PIN_SCHEMA)
async def nrf52_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View File

@ -19,6 +19,7 @@ from esphome.const import (
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE, CONF_VALUE,
CONF_WEB_SERVER, CONF_WEB_SERVER,
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI, DEVICE_CLASS_AQI,
DEVICE_CLASS_AREA, DEVICE_CLASS_AREA,
@ -81,6 +82,7 @@ from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
DEVICE_CLASSES = [ DEVICE_CLASSES = [
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI, DEVICE_CLASS_AQI,
DEVICE_CLASS_AREA, DEVICE_CLASS_AREA,

View File

@ -11,8 +11,6 @@ const std::string &OneWireDevice::get_address_name() {
return this->address_name_; return this->address_name_;
} }
std::string OneWireDevice::unique_id() { return "dallas-" + str_lower_case(format_hex(this->address_)); }
bool OneWireDevice::send_command_(uint8_t cmd) { bool OneWireDevice::send_command_(uint8_t cmd) {
if (!this->bus_->select(this->address_)) if (!this->bus_->select(this->address_))
return false; return false;

View File

@ -24,8 +24,6 @@ class OneWireDevice {
/// Helper to create (and cache) the name for this sensor. For example "0xfe0000031f1eaf29". /// Helper to create (and cache) the name for this sensor. For example "0xfe0000031f1eaf29".
const std::string &get_address_name(); const std::string &get_address_name();
std::string unique_id();
protected: protected:
uint64_t address_{0}; uint64_t address_{0};
OneWireBus *bus_{nullptr}; ///< pointer to OneWireBus instance OneWireBus *bus_{nullptr}; ///< pointer to OneWireBus instance

View File

@ -204,7 +204,7 @@ def add_pio_file(component: str, key: str, data: str):
cv.validate_id_name(key) cv.validate_id_name(key)
except cv.Invalid as e: except cv.Invalid as e:
raise EsphomeError( raise EsphomeError(
f"[{component}] Invalid PIO key: {key}. Allowed characters: [{ascii_letters}{digits}_]\nPlease report an issue https://github.com/esphome/issues" f"[{component}] Invalid PIO key: {key}. Allowed characters: [{ascii_letters}{digits}_]\nPlease report an issue https://github.com/esphome/esphome/issues"
) from e ) from e
CORE.data[KEY_RP2040][KEY_PIO_FILES][key] = data CORE.data[KEY_RP2040][KEY_PIO_FILES][key] = data

View File

@ -44,6 +44,10 @@ void Mutex::unlock() {}
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); } IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); } IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
// RP2040 doesn't support lwIP core locking, so this is a no-op
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
#ifdef USE_WIFI #ifdef USE_WIFI
WiFi.macAddress(mac); WiFi.macAddress(mac);

View File

@ -0,0 +1,34 @@
"""
Runtime statistics component for ESPHome.
"""
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
CODEOWNERS = ["@bdraco"]
CONF_LOG_INTERVAL = "log_interval"
runtime_stats_ns = cg.esphome_ns.namespace("runtime_stats")
RuntimeStatsCollector = runtime_stats_ns.class_("RuntimeStatsCollector")
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(RuntimeStatsCollector),
cv.Optional(
CONF_LOG_INTERVAL, default="60s"
): cv.positive_time_period_milliseconds,
}
)
async def to_code(config):
"""Generate code for the runtime statistics component."""
# Define USE_RUNTIME_STATS when this component is used
cg.add_define("USE_RUNTIME_STATS")
# Create the runtime stats instance (constructor sets global_runtime_stats)
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_log_interval(config[CONF_LOG_INTERVAL]))

View File

@ -0,0 +1,102 @@
#include "runtime_stats.h"
#ifdef USE_RUNTIME_STATS
#include "esphome/core/component.h"
#include <algorithm>
namespace esphome {
namespace runtime_stats {
RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0) {
global_runtime_stats = this;
}
void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) {
if (component == nullptr)
return;
// Check if we have cached the name for this component
auto name_it = this->component_names_cache_.find(component);
if (name_it == this->component_names_cache_.end()) {
// First time seeing this component, cache its name
const char *source = component->get_component_source();
this->component_names_cache_[component] = source;
this->component_stats_[source].record_time(duration_ms);
} else {
this->component_stats_[name_it->second].record_time(duration_ms);
}
if (this->next_log_time_ == 0) {
this->next_log_time_ = current_time + this->log_interval_;
return;
}
}
void RuntimeStatsCollector::log_stats_() {
ESP_LOGI(TAG, "Component Runtime Statistics");
ESP_LOGI(TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_);
// First collect stats we want to display
std::vector<ComponentStatPair> stats_to_display;
for (const auto &it : this->component_stats_) {
const ComponentRuntimeStats &stats = it.second;
if (stats.get_period_count() > 0) {
ComponentStatPair pair = {it.first, &stats};
stats_to_display.push_back(pair);
}
}
// Sort by period runtime (descending)
std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>());
// Log top components by period runtime
for (const auto &it : stats_to_display) {
const char *source = it.name;
const ComponentRuntimeStats *stats = it.stats;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source,
stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(),
stats->get_period_time_ms());
}
// Log total stats since boot
ESP_LOGI(TAG, "Total stats (since boot):");
// Re-sort by total runtime for all-time stats
std::sort(stats_to_display.begin(), stats_to_display.end(),
[](const ComponentStatPair &a, const ComponentStatPair &b) {
return a.stats->get_total_time_ms() > b.stats->get_total_time_ms();
});
for (const auto &it : stats_to_display) {
const char *source = it.name;
const ComponentRuntimeStats *stats = it.stats;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source,
stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
stats->get_total_time_ms());
}
}
void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) {
if (this->next_log_time_ == 0)
return;
if (current_time >= this->next_log_time_) {
this->log_stats_();
this->reset_stats_();
this->next_log_time_ = current_time + this->log_interval_;
}
}
} // namespace runtime_stats
runtime_stats::RuntimeStatsCollector *global_runtime_stats =
nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif // USE_RUNTIME_STATS

View File

@ -0,0 +1,132 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_RUNTIME_STATS
#include <map>
#include <vector>
#include <cstdint>
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
class Component; // Forward declaration
namespace runtime_stats {
static const char *const TAG = "runtime_stats";
class ComponentRuntimeStats {
public:
ComponentRuntimeStats()
: period_count_(0),
period_time_ms_(0),
period_max_time_ms_(0),
total_count_(0),
total_time_ms_(0),
total_max_time_ms_(0) {}
void record_time(uint32_t duration_ms) {
// Update period counters
this->period_count_++;
this->period_time_ms_ += duration_ms;
if (duration_ms > this->period_max_time_ms_)
this->period_max_time_ms_ = duration_ms;
// Update total counters
this->total_count_++;
this->total_time_ms_ += duration_ms;
if (duration_ms > this->total_max_time_ms_)
this->total_max_time_ms_ = duration_ms;
}
void reset_period_stats() {
this->period_count_ = 0;
this->period_time_ms_ = 0;
this->period_max_time_ms_ = 0;
}
// Period stats (reset each logging interval)
uint32_t get_period_count() const { return this->period_count_; }
uint32_t get_period_time_ms() const { return this->period_time_ms_; }
uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; }
float get_period_avg_time_ms() const {
return this->period_count_ > 0 ? this->period_time_ms_ / static_cast<float>(this->period_count_) : 0.0f;
}
// Total stats (persistent until reboot)
uint32_t get_total_count() const { return this->total_count_; }
uint32_t get_total_time_ms() const { return this->total_time_ms_; }
uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; }
float get_total_avg_time_ms() const {
return this->total_count_ > 0 ? this->total_time_ms_ / static_cast<float>(this->total_count_) : 0.0f;
}
protected:
// Period stats (reset each logging interval)
uint32_t period_count_;
uint32_t period_time_ms_;
uint32_t period_max_time_ms_;
// Total stats (persistent until reboot)
uint32_t total_count_;
uint32_t total_time_ms_;
uint32_t total_max_time_ms_;
};
// For sorting components by run time
struct ComponentStatPair {
const char *name;
const ComponentRuntimeStats *stats;
bool operator>(const ComponentStatPair &other) const {
// Sort by period time as that's what we're displaying in the logs
return stats->get_period_time_ms() > other.stats->get_period_time_ms();
}
};
class RuntimeStatsCollector {
public:
RuntimeStatsCollector();
void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; }
uint32_t get_log_interval() const { return this->log_interval_; }
void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time);
// Process any pending stats printing (should be called after component loop)
void process_pending_stats(uint32_t current_time);
protected:
void log_stats_();
void reset_stats_() {
for (auto &it : this->component_stats_) {
it.second.reset_period_stats();
}
}
// Use const char* keys for efficiency
// Custom comparator for const char* keys in map
// Without this, std::map would compare pointer addresses instead of string contents,
// causing identical component names at different addresses to be treated as different keys
struct CStrCompare {
bool operator()(const char *a, const char *b) const { return std::strcmp(a, b) < 0; }
};
std::map<const char *, ComponentRuntimeStats, CStrCompare> component_stats_;
std::map<Component *, const char *> component_names_cache_;
uint32_t log_interval_;
uint32_t next_log_time_;
};
} // namespace runtime_stats
extern runtime_stats::RuntimeStatsCollector
*global_runtime_stats; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif // USE_RUNTIME_STATS

Some files were not shown because too many files have changed in this diff Show More