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
- [ ] BK72xx
- [ ] RTL87xx
- [ ] nRF52840
## 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
- dependency-name: hypothesis
- package-ecosystem: github-actions
labels:
- "dependencies"
- "github-actions"
directory: "/"
schedule:
interval: daily
@ -20,11 +23,17 @@ updates:
- "docker/login-action"
- "docker/setup-buildx-action"
- package-ecosystem: github-actions
labels:
- "dependencies"
- "github-actions"
directory: "/.github/actions/build-image"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: github-actions
labels:
- "dependencies"
- "github-actions"
directory: "/.github/actions/restore-python"
schedule:
interval: daily

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
uses: actions/setup-python@v5.6.0
with:
python-version: "3.10"
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1

View File

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

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
uses: actions/setup-python@v5.6.0
with:
python-version: "3.10"
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1

View File

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

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

View File

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

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

View File

@ -3,12 +3,15 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#ifdef USE_ESP32
#include <esp_adc_cal.h>
#include "driver/adc.h"
#endif // USE_ESP32
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_adc/adc_oneshot.h"
#include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX
#endif // USE_ESP32
namespace esphome {
namespace adc {
@ -49,36 +52,72 @@ class Aggregator {
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
public:
#ifdef USE_ESP32
/// Set the attenuation for this pin. Only available on the ESP32.
void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; }
void set_channel1(adc1_channel_t channel) {
this->channel1_ = channel;
this->channel2_ = ADC2_CHANNEL_MAX;
}
void set_channel2(adc2_channel_t channel) {
this->channel2_ = channel;
this->channel1_ = ADC1_CHANNEL_MAX;
}
void set_autorange(bool autorange) { this->autorange_ = autorange; }
#endif // USE_ESP32
/// Update ADC values
/// Update the sensor's state by reading the current ADC value.
/// This method is called periodically based on the update interval.
void update() override;
/// Setup ADC
/// Set up the ADC sensor by initializing hardware and calibration parameters.
/// This method is called once during device initialization.
void setup() override;
/// Output the configuration details of the ADC sensor for debugging purposes.
/// This method is called during the ESPHome setup process to log the configuration.
void dump_config() override;
/// `HARDWARE_LATE` setup priority
/// Return the setup priority for this component.
/// Components with higher priority are initialized earlier during setup.
/// @return A float representing the setup priority.
float get_setup_priority() const override;
/// Set the GPIO pin to be used by the ADC sensor.
/// @param pin Pointer to an InternalGPIOPin representing the ADC input pin.
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
/// Enable or disable the output of raw ADC values (unprocessed data).
/// @param output_raw Boolean indicating whether to output raw ADC values (true) or processed values (false).
void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; }
/// Set the number of samples to be taken for ADC readings to improve accuracy.
/// A higher sample count reduces noise but increases the reading time.
/// @param sample_count The number of samples (e.g., 1, 4, 8).
void set_sample_count(uint8_t sample_count);
/// Set the sampling mode for how multiple ADC samples are combined into a single measurement.
///
/// When multiple samples are taken (controlled by set_sample_count), they can be combined
/// in one of three ways:
/// - SamplingMode::AVG: Compute the average (default)
/// - SamplingMode::MIN: Use the lowest sample value
/// - SamplingMode::MAX: Use the highest sample value
/// @param sampling_mode The desired sampling mode to use for aggregating ADC samples.
void set_sampling_mode(SamplingMode sampling_mode);
/// Perform a single ADC sampling operation and return the measured value.
/// This function handles raw readings, calibration, and averaging as needed.
/// @return The sampled value as a float.
float sample() override;
#ifdef USE_ESP8266
std::string unique_id() override;
#endif // USE_ESP8266
#ifdef USE_ESP32
/// Set the ADC attenuation level to adjust the input voltage range.
/// This determines how the ADC interprets input voltages, allowing for greater precision
/// or the ability to measure higher voltages depending on the chosen attenuation level.
/// @param attenuation The desired ADC attenuation level (e.g., ADC_ATTEN_DB_0, ADC_ATTEN_DB_11).
void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; }
/// Configure the ADC to use a specific channel on a specific ADC unit.
/// This sets the channel for single-shot or continuous ADC measurements.
/// @param unit The ADC unit to use (ADC_UNIT_1 or ADC_UNIT_2).
/// @param channel The ADC channel to configure, such as ADC_CHANNEL_0, ADC_CHANNEL_3, etc.
void set_channel(adc_unit_t unit, adc_channel_t channel) {
this->adc_unit_ = unit;
this->channel_ = channel;
}
/// Set whether autoranging should be enabled for the ADC.
/// Autoranging automatically adjusts the attenuation level to handle a wide range of input voltages.
/// @param autorange Boolean indicating whether to enable autoranging.
void set_autorange(bool autorange) { this->autorange_ = autorange; }
#endif // USE_ESP32
#ifdef USE_RP2040
void set_is_temperature() { this->is_temperature_ = true; }
@ -90,17 +129,28 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
InternalGPIOPin *pin_;
SamplingMode sampling_mode_{SamplingMode::AVG};
#ifdef USE_ESP32
float sample_autorange_();
float sample_fixed_attenuation_();
bool autorange_{false};
adc_oneshot_unit_handle_t adc_handle_{nullptr};
adc_cali_handle_t calibration_handle_{nullptr};
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
adc_channel_t channel_;
adc_unit_t adc_unit_;
struct SetupFlags {
uint8_t init_complete : 1;
uint8_t config_complete : 1;
uint8_t handle_init_complete : 1;
uint8_t calibration_complete : 1;
uint8_t reserved : 4;
} setup_flags_{};
static adc_oneshot_unit_handle_t shared_adc_handles[2];
#endif // USE_ESP32
#ifdef USE_RP2040
bool is_temperature_{false};
#endif // USE_RP2040
#ifdef USE_ESP32
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
adc1_channel_t channel1_{ADC1_CHANNEL_MAX};
adc2_channel_t channel2_{ADC2_CHANNEL_MAX};
bool autorange_{false};
esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {};
#endif // USE_ESP32
};
} // namespace adc

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -18,11 +18,11 @@ class APIServerConnectionBase : public ProtoService {
public:
#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
this->log_send_message_(msg.message_name(), msg.dump());
#endif
return this->send_message_(msg, T::MESSAGE_TYPE);
return this->send_message_(msg, message_type);
}
virtual void on_hello_request(const HelloRequest &value){};

View File

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

View File

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

View File

@ -11,6 +11,18 @@ namespace esphome {
namespace api {
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
private:
// Helper to convert value to string - handles the case where value is already a string
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
// Overloads for string types - needed because std::to_string doesn't support them
static std::string value_to_string(char *val) {
return val ? std::string(val) : std::string();
} // For lambdas returning char* (e.g., itoa)
static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str()
static std::string value_to_string(const std::string &val) { return val; }
static std::string value_to_string(std::string &&val) { return std::move(val); }
public:
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
@ -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>
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 {

View File

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

View File

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

View File

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

View File

@ -16,6 +16,8 @@ class UserServiceDescriptor {
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
bool is_internal() { return false; }
};
template<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)
cg.add(var.set_active(config[CONF_ACTIVE]))
await esp32_ble_tracker.register_ble_device(var, config)
await esp32_ble_tracker.register_raw_ble_device(var, config)
for connection_conf in config.get(CONF_CONNECTIONS, []):
connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
await cg.register_component(connection_var, connection_conf)
cg.add(var.register_connection(connection_var))
await esp32_ble_tracker.register_client(connection_var, connection_conf)
await esp32_ble_tracker.register_raw_client(connection_var, connection_conf)
if config.get(CONF_CACHE_SERVICES):
add_idf_sdkconfig_option("CONFIG_BT_GATTC_CACHE_NVS_FLASH", True)

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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():
# Work around Python 3.10 bug with nested async comprehensions
# With Python 3.11 this could be simplified
# TODO: Now that we require Python 3.11+, this can be updated to use nested comprehensions
styles = {}
for part, states in collect_parts(style).items():
styles[part] = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,489 +2,5 @@
#include "esphome/core/log.h"
namespace esphome {
namespace mipi_spi {
void MipiSpi::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
this->spi_setup();
if (this->dc_pin_ != nullptr) {
this->dc_pin_->setup();
this->dc_pin_->digital_write(false);
}
for (auto *pin : this->enable_pins_) {
pin->setup();
pin->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
}
this->bus_width_ = this->parent_->get_bus_width();
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
auto when = millis() + 120;
delay(10);
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
ESP_LOGD(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
auto arg_byte = vec[index];
switch (cmd) {
case SLEEP_OUT: {
// are we ready, boots?
int duration = when - millis();
if (duration > 0) {
ESP_LOGD(TAG, "Sleep %dms", duration);
delay(duration);
}
} break;
case INVERT_ON:
this->invert_colors_ = true;
break;
case MADCTL_CMD:
this->madctl_ = arg_byte;
break;
case PIXFMT:
this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18;
break;
case BRIGHTNESS:
this->brightness_ = arg_byte;
break;
default:
break;
}
const auto *ptr = vec.data() + index;
ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
this->write_command_(cmd, ptr, num_args);
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
}
}
this->setup_complete_ = true;
if (this->draw_from_origin_)
check_buffer_();
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
}
void MipiSpi::update() {
if (!this->setup_complete_ || this->is_failed()) {
return;
}
this->do_update_();
if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
return;
ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_);
// Some chips require that the drawing window be aligned on certain boundaries
auto dr = this->draw_rounding_;
this->x_low_ = this->x_low_ / dr * dr;
this->y_low_ = this->y_low_ / dr * dr;
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
if (this->draw_from_origin_) {
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_ - 1;
}
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_,
this->width_ - w - this->x_low_);
// invalidate watermarks
this->x_low_ = this->width_;
this->y_low_ = this->height_;
this->x_high_ = 0;
this->y_high_ = 0;
}
void MipiSpi::fill(Color color) {
if (!this->check_buffer_())
return;
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->get_width_internal() - 1;
this->y_high_ = this->get_height_internal() - 1;
switch (this->color_depth_) {
case display::COLOR_BITNESS_332: {
auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
break;
}
default: {
auto new_color = display::ColorUtil::color_to_565(color);
if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) {
// Upper and lower is equal can use quicker memset operation. Takes ~20ms.
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
} else {
auto *ptr_16 = reinterpret_cast<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 mipi_spi {} // namespace mipi_spi
} // namespace esphome

View File

@ -4,40 +4,39 @@
#include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/display_color_utils.h"
namespace esphome {
namespace mipi_spi {
constexpr static const char *const TAG = "display.mipi_spi";
static const uint8_t SW_RESET_CMD = 0x01;
static const uint8_t SLEEP_OUT = 0x11;
static const uint8_t NORON = 0x13;
static const uint8_t INVERT_OFF = 0x20;
static const uint8_t INVERT_ON = 0x21;
static const uint8_t ALL_ON = 0x23;
static const uint8_t WRAM = 0x24;
static const uint8_t MIPI = 0x26;
static const uint8_t DISPLAY_ON = 0x29;
static const uint8_t RASET = 0x2B;
static const uint8_t CASET = 0x2A;
static const uint8_t WDATA = 0x2C;
static const uint8_t TEON = 0x35;
static const uint8_t MADCTL_CMD = 0x36;
static const uint8_t PIXFMT = 0x3A;
static const uint8_t BRIGHTNESS = 0x51;
static const uint8_t SWIRE1 = 0x5A;
static const uint8_t SWIRE2 = 0x5B;
static const uint8_t PAGESEL = 0xFE;
static constexpr uint8_t SW_RESET_CMD = 0x01;
static constexpr uint8_t SLEEP_OUT = 0x11;
static constexpr uint8_t NORON = 0x13;
static constexpr uint8_t INVERT_OFF = 0x20;
static constexpr uint8_t INVERT_ON = 0x21;
static constexpr uint8_t ALL_ON = 0x23;
static constexpr uint8_t WRAM = 0x24;
static constexpr uint8_t MIPI = 0x26;
static constexpr uint8_t DISPLAY_ON = 0x29;
static constexpr uint8_t RASET = 0x2B;
static constexpr uint8_t CASET = 0x2A;
static constexpr uint8_t WDATA = 0x2C;
static constexpr uint8_t TEON = 0x35;
static constexpr uint8_t MADCTL_CMD = 0x36;
static constexpr uint8_t PIXFMT = 0x3A;
static constexpr uint8_t BRIGHTNESS = 0x51;
static constexpr uint8_t SWIRE1 = 0x5A;
static constexpr uint8_t SWIRE2 = 0x5B;
static constexpr uint8_t PAGESEL = 0xFE;
static const uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
static const uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
static const uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
static const uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
static const uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
static const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
static const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static const uint8_t DELAY_FLAG = 0xFF;
// store a 16 bit value in a buffer, big endian.
@ -46,28 +45,44 @@ static inline void put16_be(uint8_t *buf, uint16_t value) {
buf[1] = value;
}
// Buffer mode, conveniently also the number of bytes in a pixel
enum PixelMode {
PIXEL_MODE_16,
PIXEL_MODE_18,
PIXEL_MODE_8 = 1,
PIXEL_MODE_16 = 2,
PIXEL_MODE_18 = 3,
};
class MipiSpi : public display::DisplayBuffer,
enum BusType {
BUS_TYPE_SINGLE = 1,
BUS_TYPE_QUAD = 4,
BUS_TYPE_OCTAL = 8,
BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer
};
/**
* Base class for MIPI SPI displays.
* All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file.
*
* @tparam BUFFERTYPE The type of the buffer pixels, e.g. uint8_t or uint16_t
* @tparam BUFFERPIXEL Color depth of the buffer
* @tparam DISPLAYPIXEL Color depth of the display
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
* @tparam WIDTH Width of the display in pixels
* @tparam HEIGHT Height of the display in pixels
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
* buffer
*/
template<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,
spi::DATA_RATE_1MHZ> {
public:
MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth)
: width_(width),
height_(height),
offset_width_(offset_width),
offset_height_(offset_height),
color_depth_(color_depth) {}
MipiSpi() {}
void update() override { this->stop_poller(); }
void draw_pixel_at(int x, int y, Color color) override {}
void set_model(const char *model) { this->model_ = model; }
void update() override;
void setup() override;
display::ColorOrder get_color_mode() {
return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB;
}
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; }
@ -79,93 +94,524 @@ class MipiSpi : public display::DisplayBuffer,
this->brightness_ = brightness;
this->reset_params_();
}
void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; }
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
void dump_config() override;
int get_width_internal() override { return this->width_; }
int get_height_internal() override { return this->height_; }
bool can_proceed() override { return this->setup_complete_; }
int get_width_internal() override { return WIDTH; }
int get_height_internal() override { return HEIGHT; }
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; }
// reset the display, and write the init sequence
void setup() override {
this->spi_setup();
if (this->dc_pin_ != nullptr) {
this->dc_pin_->setup();
this->dc_pin_->digital_write(false);
}
for (auto *pin : this->enable_pins_) {
pin->setup();
pin->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
}
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
auto when = millis() + 120;
delay(10);
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
esph_log_e(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
esph_log_d(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
esph_log_e(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
auto arg_byte = vec[index];
switch (cmd) {
case SLEEP_OUT: {
// are we ready, boots?
int duration = when - millis();
if (duration > 0) {
esph_log_d(TAG, "Sleep %dms", duration);
delay(duration);
}
} break;
case INVERT_ON:
this->invert_colors_ = true;
break;
case MADCTL_CMD:
this->madctl_ = arg_byte;
break;
case BRIGHTNESS:
this->brightness_ = arg_byte;
break;
default:
break;
}
const auto *ptr = vec.data() + index;
esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
this->write_command_(cmd, ptr, num_args);
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
}
}
// init sequence no longer needed
this->init_sequence_.clear();
}
// Drawing operations
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override {
if (this->is_failed())
return;
if (w <= 0 || h <= 0)
return;
if (get_pixel_mode(bitness) != BUFFERPIXEL || big_endian != IS_BIG_ENDIAN) {
// note that the usual logging macros are banned in header files, so use their replacement
esph_log_e(TAG, "Unsupported color depth or bit order");
return;
}
this->write_to_display_(x_start, y_start, w, h, reinterpret_cast<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:
bool check_buffer_() {
if (this->is_failed())
return false;
if (this->buffer_ != nullptr)
return true;
auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1;
this->init_internal_(this->width_ * this->height_ * bytes_per_pixel);
if (this->buffer_ == nullptr) {
this->mark_failed();
return false;
}
this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel;
return true;
}
void fill(Color color) override;
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
void write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride);
void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad);
/**
* the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the
* sample code.)
*
* Immediately after enabling /CS send 4 bytes in single-dataline SPI mode:
* 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be
* sent in 1-dataline SPI. The second indicates quad mode.
* 1: 0x00
* 2: The command (register address) byte.
* 3: 0x00
*
* This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte.
* At the conclusion of the write, de-assert /CS.
*
* @param cmd
* @param bytes
* @param len
*/
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len);
/* METHODS */
// convenience functions to write commands with or without data
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
void reset_params_();
void write_init_sequence_();
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
// Writes a command to the display, with the given bytes.
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->enable();
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
this->disable();
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
this->disable();
}
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
this->enable();
this->write_array(bytes, len);
this->disable();
}
} else if constexpr (BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->disable();
this->dc_pin_->digital_write(true);
for (size_t i = 0; i != len; i++) {
this->enable();
this->write_byte(0);
this->write_byte(bytes[i]);
this->disable();
}
}
}
// write changed parameters to the display
void reset_params_() {
if (!this->is_ready())
return;
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
if (this->brightness_.has_value())
this->write_command_(BRIGHTNESS, this->brightness_.value());
}
// set the address window for the next data write
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
uint8_t buf[4];
x1 += OFFSET_WIDTH;
x2 += OFFSET_WIDTH;
y1 += OFFSET_HEIGHT;
y2 += OFFSET_HEIGHT;
put16_be(buf, y1);
put16_be(buf + 2, y2);
this->write_command_(RASET, buf, sizeof buf);
put16_be(buf, x1);
put16_be(buf + 2, x2);
this->write_command_(CASET, buf, sizeof buf);
if constexpr (BUS_TYPE != BUS_TYPE_QUAD) {
this->write_command_(WDATA);
}
}
// map the display color bitness to the pixel mode
static PixelMode get_pixel_mode(display::ColorBitness bitness) {
switch (bitness) {
case display::COLOR_BITNESS_888:
return PIXEL_MODE_18; // 18 bits per pixel
case display::COLOR_BITNESS_565:
return PIXEL_MODE_16; // 16 bits per pixel
default:
return PIXEL_MODE_8; // Default to 8 bits per pixel
}
}
/**
* Writes a buffer to the display.
* @param w Width of each line in bytes
* @param h Height of the buffer in rows
* @param pad Padding in bytes after each line
*/
void write_display_data_(const uint8_t *ptr, size_t w, size_t h, size_t pad) {
if (pad == 0) {
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->write_array(ptr, w * h);
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h, 4);
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8);
}
} else {
for (size_t y = 0; y != h; y++) {
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
this->write_array(ptr, w);
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w, 4);
} else if constexpr (BUS_TYPE == BUS_TYPE_OCTAL) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w, 8);
}
ptr += w + pad;
}
}
}
/**
* Writes a buffer to the display.
*
* The ptr is a pointer to the pixel data
* The other parameters are all in pixel units.
*/
void write_to_display_(int x_start, int y_start, int w, int h, const BUFFERTYPE *ptr, int x_offset, int y_offset,
int x_pad) {
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
this->enable();
ptr += y_offset * (x_offset + w + x_pad) + x_offset;
if constexpr (BUFFERPIXEL == DISPLAYPIXEL) {
this->write_display_data_(reinterpret_cast<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};
std::vector<GPIOPin *> enable_pins_{};
GPIOPin *dc_pin_{nullptr};
uint16_t x_low_{1};
uint16_t y_low_{1};
uint16_t x_high_{0};
uint16_t y_high_{0};
bool setup_complete_{};
// other properties set by configuration
bool invert_colors_{};
size_t width_;
size_t height_;
int16_t offset_width_;
int16_t offset_height_;
size_t buffer_bytes_{0};
display::ColorBitness color_depth_;
PixelMode pixel_mode_{PIXEL_MODE_16};
uint8_t bus_width_{};
bool spi_16_{};
uint8_t madctl_{};
bool draw_from_origin_{false};
unsigned draw_rounding_{2};
optional<uint8_t> brightness_{};
const char *model_{"Unknown"};
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 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 = {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -46,7 +46,6 @@ class MQTTSensorComponent : public mqtt::MQTTComponent {
/// Override for MQTTComponent, returns "sensor".
std::string component_type() const override;
const EntityBase *get_entity() const override;
std::string unique_id() override;
sensor::Sensor *sensor_;
optional<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"; }
const EntityBase *MQTTTextSensor::get_entity() const { return this->sensor_; }
std::string MQTTTextSensor::unique_id() { return this->sensor_->unique_id(); }
} // namespace mqtt
} // namespace esphome

View File

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

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_VALUE,
CONF_WEB_SERVER,
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI,
DEVICE_CLASS_AREA,
@ -81,6 +82,7 @@ from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@esphome/core"]
DEVICE_CLASSES = [
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI,
DEVICE_CLASS_AREA,

View File

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

View File

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

View File

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

View File

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

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