mirror of
https://github.com/esphome/esphome.git
synced 2025-07-30 07:06:38 +00:00
Merge remote-tracking branch 'upstream/dev' into memory
This commit is contained in:
commit
5503d9d7ee
222
.ai/instructions.md
Normal file
222
.ai/instructions.md
Normal 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.
|
@ -1 +1 @@
|
||||
07f621354fe1350ba51953c80273cd44a04aa44f15cc30bd7b8fe2a641427b7a
|
||||
0c2acbc16bfb7d63571dbe7042f94f683be25e4ca8a0f158a960a94adac4b931
|
||||
|
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -26,6 +26,7 @@
|
||||
- [ ] RP2040
|
||||
- [ ] BK72xx
|
||||
- [ ] RTL87xx
|
||||
- [ ] nRF52840
|
||||
|
||||
## Example entry for `config.yaml`:
|
||||
|
||||
|
1
.github/copilot-instructions.md
vendored
Symbolic link
1
.github/copilot-instructions.md
vendored
Symbolic link
@ -0,0 +1 @@
|
||||
../.ai/instructions.md
|
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@ -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
449
.github/workflows/auto-label-pr.yml
vendored
Normal 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);
|
||||
}
|
||||
}
|
2
.github/workflows/ci-docker.yml
vendored
2
.github/workflows/ci-docker.yml
vendored
@ -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
|
||||
|
||||
|
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
264
.github/workflows/codeowner-review-request.yml
vendored
Normal file
264
.github/workflows/codeowner-review-request.yml
vendored
Normal 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);
|
||||
}
|
147
.github/workflows/external-component-bot.yml
vendored
Normal file
147
.github/workflows/external-component-bot.yml
vendored
Normal 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);
|
||||
}
|
119
.github/workflows/issue-codeowner-notify.yml
vendored
Normal file
119
.github/workflows/issue-codeowner-notify.yml
vendored
Normal 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);
|
||||
}
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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"];
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
@ -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
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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){};
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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_);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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")
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
),
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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_;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
88
esphome/components/logger/logger_zephyr.cpp
Normal file
88
esphome/components/logger/logger_zephyr.cpp
Normal 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
|
@ -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] = {
|
||||
|
@ -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))
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
30
esphome/components/mipi_spi/models/adafruit.py
Normal file
30
esphome/components/mipi_spi/models/adafruit.py
Normal 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 = {}
|
@ -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 = {}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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_;
|
||||
};
|
||||
|
218
esphome/components/nrf52/__init__.py
Normal file
218
esphome/components/nrf52/__init__.py
Normal 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
|
34
esphome/components/nrf52/boards.py
Normal file
34
esphome/components/nrf52/boards.py
Normal 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"),
|
||||
],
|
||||
}
|
4
esphome/components/nrf52/const.py
Normal file
4
esphome/components/nrf52/const.py
Normal 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"
|
53
esphome/components/nrf52/gpio.py
Normal file
53
esphome/components/nrf52/gpio.py
Normal 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
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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);
|
||||
|
34
esphome/components/runtime_stats/__init__.py
Normal file
34
esphome/components/runtime_stats/__init__.py
Normal 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]))
|
102
esphome/components/runtime_stats/runtime_stats.cpp
Normal file
102
esphome/components/runtime_stats/runtime_stats.cpp
Normal 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
|
132
esphome/components/runtime_stats/runtime_stats.h
Normal file
132
esphome/components/runtime_stats/runtime_stats.h
Normal 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
Loading…
x
Reference in New Issue
Block a user