Compare commits

..

8 Commits

Author SHA1 Message Date
J. Nick Koston
4c9e4d30e9 tweak 2025-10-19 14:56:40 -10:00
J. Nick Koston
db42983f0c wip 2025-10-19 14:54:31 -10:00
J. Nick Koston
20c65f70ed wip 2025-10-19 14:47:19 -10:00
J. Nick Koston
38e31e328c wip 2025-10-19 14:46:25 -10:00
J. Nick Koston
58cecff778 wip 2025-10-19 14:44:04 -10:00
J. Nick Koston
1946656ea8 wip 2025-10-19 14:40:47 -10:00
J. Nick Koston
c9700a0450 wip 2025-10-19 14:35:09 -10:00
J. Nick Koston
0eab64ffe5 cache github downloads 2025-10-19 14:33:26 -10:00
2504 changed files with 28231 additions and 77601 deletions

View File

@@ -51,79 +51,7 @@ This document provides essential context for AI models interacting with this pro
* **Naming Conventions:** * **Naming Conventions:**
* **Python:** Follows PEP 8. Use clear, descriptive names following snake_case. * **Python:** Follows PEP 8. Use clear, descriptive names following snake_case.
* **C++:** Follows the Google C++ Style Guide with these specifics (following clang-tidy conventions): * **C++:** Follows the Google C++ Style Guide.
- Function, method, and variable names: `lower_snake_case`
- Class/struct/enum names: `UpperCamelCase`
- Top-level constants (global/namespace scope): `UPPER_SNAKE_CASE`
- Function-local constants: `lower_snake_case`
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
- Favor descriptive names over abbreviations
* **C++ Field Visibility:**
* **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
* **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants:
1. **Pointer lifetime issues:** When setters validate and store pointers from known lists to prevent dangling references.
```cpp
// Helper to find matching string in vector and return its pointer
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
return item;
}
return nullptr;
}
class ClimateDevice {
public:
void set_custom_fan_modes(std::initializer_list<const char *> modes) {
this->custom_fan_modes_ = modes;
this->active_custom_fan_mode_ = nullptr; // Reset when modes change
}
bool set_custom_fan_mode(const char *mode) {
// Find mode in supported list and store that pointer (not the input pointer)
const char *validated_mode = vector_find(this->custom_fan_modes_, mode);
if (validated_mode != nullptr) {
this->active_custom_fan_mode_ = validated_mode;
return true;
}
return false;
}
private:
std::vector<const char *> custom_fan_modes_; // Pointers to string literals in flash
const char *active_custom_fan_mode_{nullptr}; // Must point to entry in custom_fan_modes_
};
```
2. **Invariant coupling:** When multiple fields must remain synchronized to prevent buffer overflows or data corruption.
```cpp
class Buffer {
public:
void resize(size_t new_size) {
auto new_data = std::make_unique<uint8_t[]>(new_size);
if (this->data_) {
std::memcpy(new_data.get(), this->data_.get(), std::min(this->size_, new_size));
}
this->data_ = std::move(new_data);
this->size_ = new_size; // Must stay in sync with data_
}
private:
std::unique_ptr<uint8_t[]> data_;
size_t size_{0}; // Must match allocated size of data_
};
```
3. **Resource management:** When setters perform cleanup or registration operations that derived classes might skip.
* **Provide `protected` accessor methods:** When derived classes need controlled access to `private` members.
* **C++ Preprocessor Directives:**
* **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums.
* **Use `#define` only for:**
- Conditional compilation (`#ifdef`, `#ifndef`)
- Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`)
* **C++ Additional Conventions:**
* **Member access:** Prefix all class member access with `this->` (e.g., `this->value_` not `value_`)
* **Indentation:** Use spaces (two per indentation level), not tabs
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
* **Line length:** Wrap lines at no more than 120 characters
* **Component Structure:** * **Component Structure:**
* **Standard Files:** * **Standard Files:**
@@ -172,7 +100,8 @@ This document provides essential context for AI models interacting with this pro
* **C++ Class Pattern:** * **C++ Class Pattern:**
```cpp ```cpp
namespace esphome::my_component { namespace esphome {
namespace my_component {
class MyComponent : public Component { class MyComponent : public Component {
public: public:
@@ -188,7 +117,8 @@ This document provides essential context for AI models interacting with this pro
int param_{0}; int param_{0};
}; };
} // namespace esphome::my_component } // namespace my_component
} // namespace esphome
``` ```
* **Common Component Examples:** * **Common Component Examples:**
@@ -276,12 +206,12 @@ This document provides essential context for AI models interacting with this pro
## 7. Specific Instructions for AI Collaboration ## 7. Specific Instructions for AI Collaboration
* **Contribution Workflow (Pull Request Process):** * **Contribution Workflow (Pull Request Process):**
1. **Fork & Branch:** Create a new branch based on the `dev` branch (always use `git checkout -b <branch-name> dev` to ensure you're branching from `dev`, not the currently checked out branch). 1. **Fork & Branch:** Create a new branch in your fork.
2. **Make Changes:** Adhere to all coding conventions and patterns. 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. 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. 4. **Lint:** Run `pre-commit` to ensure code is compliant.
5. **Commit:** Commit your changes. There is no strict format for commit messages. 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 using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template. 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 Contributions:**
* Documentation is hosted in the separate `esphome/esphome-docs` repository. * Documentation is hosted in the separate `esphome/esphome-docs` repository.
@@ -293,12 +223,6 @@ This document provides essential context for AI models interacting with this pro
* **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization. * **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization.
* **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage. * **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage.
**Why Heap Allocation Matters:**
ESP devices run for months with small heaps shared between Wi-Fi, BLE, LWIP, and application code. Over time, repeated allocations of different sizes fragment the heap. Failures happen when the largest contiguous block shrinks, even if total free heap is still large. We have seen field crashes caused by this.
**Heap allocation after `setup()` should be avoided unless absolutely unavoidable.** Every allocation/deallocation cycle contributes to fragmentation. ESPHome treats runtime heap allocation as a long-term reliability bug, not a performance issue. Helpers that hide allocation (`std::string`, `std::to_string`, string-returning helpers) are being deprecated and replaced with buffer and view based APIs.
**STL Container Guidelines:** **STL Container Guidelines:**
ESPHome runs on embedded systems with limited resources. Choose containers carefully: ESPHome runs on embedded systems with limited resources. Choose containers carefully:
@@ -328,15 +252,15 @@ This document provides essential context for AI models interacting with this pro
std::array<uint8_t, 256> buffer; std::array<uint8_t, 256> buffer;
``` ```
2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for compile-time fixed size with `push_back()` interface (no dynamic allocation). 2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for fixed-size stack allocation with `push_back()` interface.
```cpp ```cpp
// Bad - generates STL realloc code (_M_realloc_insert) // Bad - generates STL realloc code (_M_realloc_insert)
std::vector<ServiceRecord> services; std::vector<ServiceRecord> services;
services.reserve(5); // Still includes reallocation machinery services.reserve(5); // Still includes reallocation machinery
// Good - compile-time fixed size, no dynamic allocation // Good - compile-time fixed size, stack allocated, no reallocation machinery
StaticVector<ServiceRecord, MAX_SERVICES> services; StaticVector<ServiceRecord, MAX_SERVICES> services; // Allocates all MAX_SERVICES on stack
services.push_back(record1); services.push_back(record1); // Tracks count but all slots allocated
``` ```
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration. Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code. Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
@@ -378,21 +302,22 @@ This document provides essential context for AI models interacting with this pro
``` ```
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise. Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
5. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices. 5. **Detection:** Look for these patterns in compiler output:
6. **Detection:** Look for these patterns in compiler output:
- Large code sections with STL symbols (vector, map, set) - Large code sections with STL symbols (vector, map, set)
- `alloc`, `realloc`, `dealloc` in symbol names - `alloc`, `realloc`, `dealloc` in symbol names
- `_M_realloc_insert`, `_M_default_append` (vector reallocation) - `_M_realloc_insert`, `_M_default_append` (vector reallocation)
- Red-black tree code (`rb_tree`, `_Rb_tree`) - Red-black tree code (`rb_tree`, `_Rb_tree`)
- Hash table infrastructure (`unordered_map`, `hash`) - Hash table infrastructure (`unordered_map`, `hash`)
**Prioritize optimization effort for:** **When to optimize:**
- Core components (API, network, logger) - Core components (API, network, logger)
- Widely-used components (mdns, wifi, ble) - Widely-used components (mdns, wifi, ble)
- Components causing flash size complaints - Components causing flash size complaints
Note: Avoiding heap allocation after `setup()` is always required regardless of component type. The prioritization above is about the effort spent on container optimization (e.g., migrating from `std::vector` to `StaticVector`). **When not to optimize:**
- Single-use niche components
- Code where readability matters more than bytes
- Already using appropriate containers
* **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals. * **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.
@@ -407,45 +332,35 @@ This document provides essential context for AI models interacting with this pro
_use_feature = True _use_feature = True
``` ```
**Bad Pattern (Flat Keys):** **Good Pattern (CORE.data with Helpers):**
```python ```python
# Don't do this - keys should be namespaced under component domain
MY_FEATURE_KEY = "my_component_feature"
CORE.data[MY_FEATURE_KEY] = True
```
**Good Pattern (dataclass):**
```python
from dataclasses import dataclass, field
from esphome.core import CORE from esphome.core import CORE
DOMAIN = "my_component" # Keys for CORE.data storage
COMPONENT_STATE_KEY = "my_component_state"
USE_FEATURE_KEY = "my_component_use_feature"
@dataclass def _get_component_state() -> list:
class MyComponentData: """Get component state from CORE.data."""
feature_enabled: bool = False return CORE.data.setdefault(COMPONENT_STATE_KEY, [])
item_count: int = 0
items: list[str] = field(default_factory=list)
def _get_data() -> MyComponentData: def _get_use_feature() -> bool | None:
if DOMAIN not in CORE.data: """Get feature flag from CORE.data."""
CORE.data[DOMAIN] = MyComponentData() return CORE.data.get(USE_FEATURE_KEY)
return CORE.data[DOMAIN]
def request_feature() -> None: def _set_use_feature(value: bool) -> None:
_get_data().feature_enabled = True """Set feature flag in CORE.data."""
CORE.data[USE_FEATURE_KEY] = value
def add_item(item: str) -> None: def enable_feature():
_get_data().items.append(item) _set_use_feature(True)
``` ```
If you need a real-world example, search for components that use `@dataclass` with `CORE.data` in the codebase. Note: Some components may use `TypedDict` for dictionary-based storage; both patterns are acceptable depending on your needs.
**Why this matters:** **Why this matters:**
- Module-level globals persist between compilation runs if the dashboard doesn't fork/exec - Module-level globals persist between compilation runs if the dashboard doesn't fork/exec
- `CORE.data` automatically clears between runs - `CORE.data` automatically clears between runs
- Namespacing under `DOMAIN` prevents key collisions between components - Typed helper functions provide better IDE support and maintainability
- `@dataclass` provides type safety and cleaner attribute access - Encapsulation makes state management explicit and testable
* **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. * **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.
@@ -453,45 +368,3 @@ This document provides essential context for AI models interacting with this pro
* **Python:** When adding a new Python dependency, add it to the appropriate `requirements*.txt` file and `pyproject.toml`. * **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`. * **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. * **Build Flags:** Use `cg.add_build_flag(...)` to add compiler flags.
## 8. Public API and Breaking Changes
* **Public C++ API:**
* **Components**: Only documented features at [esphome.io](https://esphome.io) are public API. Undocumented `public` members are internal.
* **Core/Base Classes** (`esphome/core/`, `Component`, `Sensor`, etc.): All `public` members are public API.
* **Components with Global Accessors** (`global_api_server`, etc.): All `public` members are public API (except config setters).
* **Public Python API:**
* All documented configuration options at [esphome.io](https://esphome.io) are public API.
* Python code in `esphome/core/` actively used by existing core components is considered stable API.
* Other Python code is internal unless explicitly documented for external component use.
* **Breaking Changes Policy:**
* Aim for **6-month deprecation window** when possible
* Clean breaks allowed for: signature changes, deep refactorings, resource constraints
* Must document migration path in PR description (generates release notes)
* Blog post required for core/base class changes or significant architectural changes
* Full details: https://developers.esphome.io/contributing/code/#public-api-and-breaking-changes
* **Breaking Change Checklist:**
- [ ] Clear justification (RAM/flash savings, architectural improvement)
- [ ] Explored non-breaking alternatives
- [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++)
- [ ] Documented migration path in PR description with before/after examples
- [ ] Updated all internal usage and esphome-docs
- [ ] Tested backward compatibility during deprecation period
* **Deprecation Pattern (C++):**
```cpp
// Remove before 2026.6.0
ESPDEPRECATED("Use new_method() instead. Removed in 2026.6.0", "2025.12.0")
void old_method() { this->new_method(); }
```
* **Deprecation Pattern (Python):**
```python
# Remove before 2026.6.0
if CONF_OLD_KEY in config:
_LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0")
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
```

View File

@@ -1 +1 @@
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65 d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248

View File

@@ -7,7 +7,6 @@
- [ ] Bugfix (non-breaking change which fixes an issue) - [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality) - [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Developer breaking change (an API change that could break external components)
- [ ] Code quality improvements to existing code or addition of tests - [ ] Code quality improvements to existing code or addition of tests
- [ ] Other - [ ] Other

View File

@@ -17,12 +17,12 @@ runs:
steps: steps:
- name: Set up Python ${{ inputs.python-version }} - name: Set up Python ${{ inputs.python-version }}
id: python id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length

View File

@@ -22,11 +22,11 @@ jobs:
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with: with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -53,7 +53,6 @@ jobs:
'new-target-platform', 'new-target-platform',
'merging-to-release', 'merging-to-release',
'merging-to-beta', 'merging-to-beta',
'chained-pr',
'core', 'core',
'small-pr', 'small-pr',
'dashboard', 'dashboard',
@@ -68,7 +67,6 @@ jobs:
'bugfix', 'bugfix',
'new-feature', 'new-feature',
'breaking-change', 'breaking-change',
'developer-breaking-change',
'code-quality' 'code-quality'
]; ];
@@ -142,8 +140,6 @@ jobs:
labels.add('merging-to-release'); labels.add('merging-to-release');
} else if (baseRef === 'beta') { } else if (baseRef === 'beta') {
labels.add('merging-to-beta'); labels.add('merging-to-beta');
} else if (baseRef !== 'dev') {
labels.add('chained-pr');
} }
return labels; return labels;
@@ -368,7 +364,6 @@ jobs:
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' }, { pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' }, { pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' }, { pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' } { pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
]; ];
@@ -418,7 +413,7 @@ jobs:
} }
// Generate review messages // Generate review messages
function generateReviewMessages(finalLabels, originalLabelCount) { function generateReviewMessages(finalLabels) {
const messages = []; const messages = [];
const prAuthor = context.payload.pull_request.user.login; const prAuthor = context.payload.pull_request.user.login;
@@ -432,15 +427,15 @@ jobs:
.reduce((sum, file) => sum + (file.deletions || 0), 0); .reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
const tooManyLabels = originalLabelCount > MAX_LABELS; const tooManyLabels = finalLabels.length > MAX_LABELS;
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`; let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
if (tooManyLabels && tooManyChanges) { if (tooManyLabels && tooManyChanges) {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`; message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`;
} else if (tooManyLabels) { } else if (tooManyLabels) {
message += `This PR affects ${originalLabelCount} different components/areas.`; message += `This PR affects ${finalLabels.length} different components/areas.`;
} else { } else {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`; message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
} }
@@ -468,8 +463,8 @@ jobs:
} }
// Handle reviews // Handle reviews
async function handleReviews(finalLabels, originalLabelCount) { async function handleReviews(finalLabels) {
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount); const reviewMessages = generateReviewMessages(finalLabels);
const hasReviewableLabels = finalLabels.some(label => const hasReviewableLabels = finalLabels.some(label =>
['too-big', 'needs-codeowners'].includes(label) ['too-big', 'needs-codeowners'].includes(label)
); );
@@ -533,8 +528,8 @@ jobs:
const apiData = await fetchApiData(); const apiData = await fetchApiData();
const baseRef = context.payload.pull_request.base.ref; const baseRef = context.payload.pull_request.base.ref;
// Early exit for release and beta branches only // Early exit for non-dev branches
if (baseRef === 'release' || baseRef === 'beta') { if (baseRef !== 'dev') {
const branchLabels = await detectMergeBranch(); const branchLabels = await detectMergeBranch();
const finalLabels = Array.from(branchLabels); const finalLabels = Array.from(branchLabels);
@@ -629,7 +624,6 @@ jobs:
// Handle too many labels (only for non-mega PRs) // Handle too many labels (only for non-mega PRs)
const tooManyLabels = finalLabels.length > MAX_LABELS; const tooManyLabels = finalLabels.length > MAX_LABELS;
const originalLabelCount = finalLabels.length;
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) { if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
finalLabels = ['too-big']; finalLabels = ['too-big'];
@@ -638,7 +632,7 @@ jobs:
console.log('Computed labels:', finalLabels.join(', ')); console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews // Handle reviews
await handleReviews(finalLabels, originalLabelCount); await handleReviews(finalLabels);
// Apply labels // Apply labels
if (finalLabels.length > 0) { if (finalLabels.length > 0) {

View File

@@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.11" python-version: "3.11"
@@ -62,7 +62,7 @@ jobs:
run: git diff run: git diff
- if: failure() - if: failure()
name: Archive artifacts name: Archive artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: generated-proto-files name: generated-proto-files
path: | path: |

View File

@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.11" python-version: "3.11"

View File

@@ -43,13 +43,13 @@ jobs:
- "docker" - "docker"
# - "lint" # - "lint"
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.11" python-version: "3.11"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Set TAG - name: Set TAG
run: | run: |

View File

@@ -28,28 +28,25 @@ jobs:
run: | run: |
# Get PR details by searching for PR with matching head SHA # Get PR details by searching for PR with matching head SHA
# The workflow_run.pull_requests field is often empty for forks # The workflow_run.pull_requests field is often empty for forks
# Use paginate to handle repos with many open PRs
head_sha="${{ github.event.workflow_run.head_sha }}" head_sha="${{ github.event.workflow_run.head_sha }}"
pr_data=$(gh api --paginate "/repos/${{ github.repository }}/pulls" \ pr_data=$(gh api "/repos/${{ github.repository }}/commits/$head_sha/pulls" \
--jq ".[] | select(.head.sha == \"$head_sha\") | {number: .number, base_ref: .base.ref}" \ --jq '.[0] | {number: .number, base_ref: .base.ref}')
| head -n 1) if [ -z "$pr_data" ] || [ "$pr_data" == "null" ]; then
if [ -z "$pr_data" ]; then
echo "No PR found for SHA $head_sha, skipping" echo "No PR found for SHA $head_sha, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT" echo "skip=true" >> $GITHUB_OUTPUT
exit 0 exit 0
fi fi
pr_number=$(echo "$pr_data" | jq -r '.number') pr_number=$(echo "$pr_data" | jq -r '.number')
base_ref=$(echo "$pr_data" | jq -r '.base_ref') base_ref=$(echo "$pr_data" | jq -r '.base_ref')
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" echo "pr_number=$pr_number" >> $GITHUB_OUTPUT
echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT" echo "base_ref=$base_ref" >> $GITHUB_OUTPUT
echo "Found PR #$pr_number targeting base branch: $base_ref" echo "Found PR #$pr_number targeting base branch: $base_ref"
- name: Check out code from base repository - name: Check out code from base repository
if: steps.pr.outputs.skip != 'true' if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
# Always check out from the base repository (esphome/esphome), never from forks # Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo # Use the PR's target branch to ensure we run trusted code from the main repo
@@ -90,9 +87,9 @@ jobs:
if: steps.pr.outputs.skip != 'true' if: steps.pr.outputs.skip != 'true'
run: | run: |
if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then
echo "found=true" >> "$GITHUB_OUTPUT" echo "found=true" >> $GITHUB_OUTPUT
else else
echo "found=false" >> "$GITHUB_OUTPUT" echo "found=false" >> $GITHUB_OUTPUT
echo "Memory analysis artifacts not found, skipping comment" echo "Memory analysis artifacts not found, skipping comment"
fi fi

View File

@@ -36,18 +36,18 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }} cache-key: ${{ steps.cache-key.outputs.key }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Generate cache-key - name: Generate cache-key
id: cache-key id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@@ -70,7 +70,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true' if: needs.determine-jobs.outputs.python-linters == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -91,7 +91,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -114,7 +114,7 @@ jobs:
matrix: matrix:
python-version: python-version:
- "3.11" - "3.11"
- "3.13" - "3.14"
os: os:
- ubuntu-latest - ubuntu-latest
- macOS-latest - macOS-latest
@@ -123,16 +123,16 @@ jobs:
# Minimize CI resource usage # Minimize CI resource usage
# by only running the Python version # by only running the Python version
# version used for docker images on Windows and macOS # version used for docker images on Windows and macOS
- python-version: "3.13" - python-version: "3.14"
os: windows-latest os: windows-latest
- python-version: "3.13" - python-version: "3.14"
os: macOS-latest os: macOS-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: needs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
id: restore-python id: restore-python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
@@ -152,12 +152,12 @@ jobs:
. venv/bin/activate . venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache - name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -170,20 +170,15 @@ jobs:
outputs: outputs:
integration-tests: ${{ steps.determine.outputs.integration-tests }} integration-tests: ${{ steps.determine.outputs.integration-tests }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }} clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
python-linters: ${{ steps.determine.outputs.python-linters }} python-linters: ${{ steps.determine.outputs.python-linters }}
changed-components: ${{ steps.determine.outputs.changed-components }} changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }} changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }} directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
component-test-count: ${{ steps.determine.outputs.component-test-count }} component-test-count: ${{ steps.determine.outputs.component-test-count }}
changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }}
memory_impact: ${{ steps.determine.outputs.memory-impact }} memory_impact: ${{ steps.determine.outputs.memory-impact }}
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
# Fetch enough history to find the merge base # Fetch enough history to find the merge base
fetch-depth: 2 fetch-depth: 2
@@ -192,11 +187,6 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
- name: Determine which tests to run - name: Determine which tests to run
id: determine id: determine
env: env:
@@ -210,23 +200,12 @@ jobs:
# Extract individual fields # Extract individual fields
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
integration-tests: integration-tests:
name: Run integration tests name: Run integration tests
@@ -237,15 +216,15 @@ jobs:
if: needs.determine-jobs.outputs.integration-tests == 'true' if: needs.determine-jobs.outputs.integration-tests == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python 3.13 - name: Set up Python 3.13
id: python id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.13" python-version: "3.13"
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -264,34 +243,7 @@ jobs:
. venv/bin/activate . venv/bin/activate
pytest -vv --no-cov --tb=native -n auto tests/integration/ pytest -vv --no-cov --tb=native -n auto tests/integration/
cpp-unit-tests: clang-tidy:
name: Run C++ unit tests
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run cpp_unit_test.py
run: |
. venv/bin/activate
if [ "${{ needs.determine-jobs.outputs.cpp-unit-tests-run-all }}" = "true" ]; then
script/cpp_unit_test.py --all
else
ARGS=$(echo '${{ needs.determine-jobs.outputs.cpp-unit-tests-components }}' | jq -r '.[] | @sh' | xargs)
script/cpp_unit_test.py $ARGS
fi
clang-tidy-single:
name: ${{ matrix.name }} name: ${{ matrix.name }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
@@ -309,6 +261,22 @@ jobs:
name: Run script/clang-tidy for ESP8266 name: Run script/clang-tidy for ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266 options: --environment esp8266-arduino-tidy --grep USE_ESP8266
pio_cache_key: tidyesp8266 pio_cache_key: tidyesp8266
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 1/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
pio_cache_key: tidyesp32
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 2/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
pio_cache_key: tidyesp32
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 3/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
pio_cache_key: tidyesp32
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 4/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
pio_cache_key: tidyesp32
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for ESP32 IDF name: Run script/clang-tidy for ESP32 IDF
options: --environment esp32-idf-tidy --grep USE_ESP_IDF options: --environment esp32-idf-tidy --grep USE_ESP_IDF
@@ -321,7 +289,7 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
# Need history for HEAD~1 to work for checking changed files # Need history for HEAD~1 to work for checking changed files
fetch-depth: 2 fetch-depth: 2
@@ -334,14 +302,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -389,165 +357,45 @@ jobs:
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
if: always() if: always()
clang-tidy-nosplit: test-build-components-splitter:
name: Run script/clang-tidy for ESP32 Arduino name: Split components for intelligent grouping (40 weighted per batch)
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- determine-jobs - determine-jobs
if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit' if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
env: outputs:
GH_TOKEN: ${{ github.token }} matrix: ${{ steps.split.outputs.components }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Split components intelligently based on bus configurations
- name: Cache platformio id: split
if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- name: Check if full clang-tidy scan needed
id: check_full_scan
run: | run: |
. venv/bin/activate . venv/bin/activate
if python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT # Use intelligent splitter that groups components with same bus configs
echo "reason=hash_changed" >> $GITHUB_OUTPUT components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
# Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed='[]'
echo "Target branch: ${{ github.base_ref }} - grouping all components"
else else
echo "full_scan=false" >> $GITHUB_OUTPUT directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
echo "reason=normal" >> $GITHUB_OUTPUT echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
fi fi
- name: Run clang-tidy echo "Splitting components intelligently..."
run: | output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
. venv/bin/activate
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (hash changed)"
script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
else
echo "Running clang-tidy on changed files only"
script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy
fi
env:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Suggested changes echo "$output" >> $GITHUB_OUTPUT
run: script/ci-suggest-changes
if: always()
clang-tidy-split:
name: ${{ matrix.name }}
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
env:
GH_TOKEN: ${{ github.token }}
strategy:
fail-fast: false
max-parallel: 2
matrix:
include:
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 1/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 2/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 3/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 4/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- name: Check if full clang-tidy scan needed
id: check_full_scan
run: |
. venv/bin/activate
if python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
echo "full_scan=false" >> $GITHUB_OUTPUT
echo "reason=normal" >> $GITHUB_OUTPUT
fi
- name: Run clang-tidy
run: |
. venv/bin/activate
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (hash changed)"
script/clang-tidy --all-headers --fix ${{ matrix.options }}
else
echo "Running clang-tidy on changed files only"
script/clang-tidy --all-headers --fix --changed ${{ matrix.options }}
fi
env:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
test-build-components-split: test-build-components-split:
name: Test components batch (${{ matrix.components }}) name: Test components batch (${{ matrix.components }})
@@ -555,12 +403,13 @@ jobs:
needs: needs:
- common - common
- determine-jobs - determine-jobs
- test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }} max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
matrix: matrix:
components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }} components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
steps: steps:
- name: Show disk space - name: Show disk space
run: | run: |
@@ -577,12 +426,27 @@ jobs:
version: 1.0 version: 1.0
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-test-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-test-${{ hashFiles('platformio.ini') }}
- name: Validate and compile components with intelligent grouping - name: Validate and compile components with intelligent grouping
run: | run: |
. venv/bin/activate . venv/bin/activate
@@ -662,13 +526,13 @@ jobs:
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache - uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
env: env:
SKIP: pylint,clang-tidy-hash SKIP: pylint,clang-tidy-hash
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
@@ -688,7 +552,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip }} skip: ${{ steps.check-script.outputs.skip }}
steps: steps:
- name: Check out target branch - name: Check out target branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
ref: ${{ github.base_ref }} ref: ${{ github.base_ref }}
@@ -735,7 +599,7 @@ jobs:
- name: Restore cached memory analysis - name: Restore cached memory analysis
id: cache-memory-analysis id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: memory-analysis-target.json path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }} key: ${{ steps.cache-key.outputs.cache-key }}
@@ -759,7 +623,7 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -800,7 +664,7 @@ jobs:
- name: Save memory analysis to cache - name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: memory-analysis-target.json path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }} key: ${{ steps.cache-key.outputs.cache-key }}
@@ -821,7 +685,7 @@ jobs:
fi fi
- name: Upload memory analysis JSON - name: Upload memory analysis JSON
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: memory-analysis-target name: memory-analysis-target
path: memory-analysis-target.json path: memory-analysis-target.json
@@ -840,14 +704,14 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }} flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps: steps:
- name: Check out PR branch - name: Check out PR branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio - name: Cache platformio
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -885,7 +749,7 @@ jobs:
--platform "$platform" --platform "$platform"
- name: Upload memory analysis JSON - name: Upload memory analysis JSON
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: memory-analysis-pr name: memory-analysis-pr
path: memory-analysis-pr.json path: memory-analysis-pr.json
@@ -908,20 +772,20 @@ jobs:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON - name: Download target analysis JSON
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: memory-analysis-target name: memory-analysis-target
path: ./memory-analysis path: ./memory-analysis
continue-on-error: true continue-on-error: true
- name: Download PR analysis JSON - name: Download PR analysis JSON
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: memory-analysis-pr name: memory-analysis-pr
path: ./memory-analysis path: ./memory-analysis
@@ -948,10 +812,9 @@ jobs:
- pylint - pylint
- pytest - pytest
- integration-tests - integration-tests
- clang-tidy-single - clang-tidy
- clang-tidy-nosplit
- clang-tidy-split
- determine-jobs - determine-jobs
- test-build-components-splitter
- test-build-components-split - test-build-components-split
- pre-commit-ci-lite - pre-commit-ci-lite
- memory-impact-target-branch - memory-impact-target-branch
@@ -959,13 +822,13 @@ jobs:
- memory-impact-comment - memory-impact-comment
if: always() if: always()
steps: steps:
- name: Check job results - name: Success
if: ${{ !(contains(needs.*.result, 'failure')) }}
run: exit 0
- name: Failure
if: ${{ contains(needs.*.result, 'failure') }}
env: env:
NEEDS_JSON: ${{ toJSON(needs) }} JSON_DOC: ${{ toJSON(needs) }}
run: | run: |
# memory-impact-target-branch is allowed to fail without blocking CI. echo $JSON_DOC | jq
# This job builds the target branch (dev/beta/release) which may fail because: exit 1
# 1. The target branch has a build issue independent of this PR
# 2. This PR fixes a build issue on the target branch
# In either case, we only care that the PR branch builds successfully.
echo "$NEEDS_JSON" | jq -e 'del(.["memory-impact-target-branch"]) | all(.result != "failure")'

View File

@@ -21,7 +21,7 @@ permissions:
jobs: jobs:
request-codeowner-reviews: request-codeowner-reviews:
name: Run name: Run
if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }} if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Request reviews from component codeowners - name: Request reviews from component codeowners

View File

@@ -54,11 +54,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }} branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }} deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Get tag - name: Get tag
id: tag id: tag
# yamllint disable rule:line-length # yamllint disable rule:line-length
@@ -60,9 +60,9 @@ jobs:
contents: read contents: read
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.x" python-version: "3.x"
- name: Build - name: Build
@@ -92,14 +92,14 @@ jobs:
os: "ubuntu-24.04-arm" os: "ubuntu-24.04-arm"
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.11" python-version: "3.11"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log in to docker hub - name: Log in to docker hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
@@ -138,7 +138,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }} # version: ${{ needs.init.outputs.tag }}
- name: Upload digests - name: Upload digests
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: digests-${{ matrix.platform.arch }} name: digests-${{ matrix.platform.arch }}
path: /tmp/digests path: /tmp/digests
@@ -168,17 +168,17 @@ jobs:
- ghcr - ghcr
- dockerhub - dockerhub
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download digests - name: Download digests
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with: with:
pattern: digests-* pattern: digests-*
path: /tmp/digests path: /tmp/digests
merge-multiple: true merge-multiple: true
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log in to docker hub - name: Log in to docker hub
if: matrix.registry == 'dockerhub' if: matrix.registry == 'dockerhub'
@@ -219,19 +219,10 @@ jobs:
- init - init
- deploy-manifest - deploy-manifest
steps: steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: home-assistant-addon
- name: Trigger Workflow - name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with: with:
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
script: | script: |
let description = "ESPHome"; let description = "ESPHome";
if (context.eventName == "release") { if (context.eventName == "release") {
@@ -254,19 +245,10 @@ jobs:
needs: [init] needs: [init]
environment: ${{ needs.init.outputs.deploy_env }} environment: ${{ needs.init.outputs.deploy_env }}
steps: steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: esphome-schema
- name: Trigger Workflow - name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with: with:
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }}
script: | script: |
github.rest.actions.createWorkflowDispatch({ github.rest.actions.createWorkflowDispatch({
owner: "esphome", owner: "esphome",
@@ -277,34 +259,3 @@ jobs:
version: "${{ needs.init.outputs.tag }}", version: "${{ needs.init.outputs.tag }}",
} }
}) })
version-notifier:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs:
- init
- deploy-manifest
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: version-notifier
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "version-notifier",
workflow_id: "notify.yml",
ref: "main",
inputs: {
version: "${{ needs.init.outputs.tag }}",
}
})

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Stale - name: Stale
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with: with:
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
remove-stale-when-updated: true remove-stale-when-updated: true

View File

@@ -14,7 +14,6 @@ jobs:
label: label:
- needs-docs - needs-docs
- merge-after-release - merge-after-release
- chained-pr
steps: steps:
- name: Check for ${{ matrix.label }} label - name: Check for ${{ matrix.label }} label
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0

View File

@@ -13,16 +13,16 @@ jobs:
if: github.repository == 'esphome/esphome' if: github.repository == 'esphome/esphome'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Checkout Home Assistant - name: Checkout Home Assistant
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
repository: home-assistant/core repository: home-assistant/core
path: lib/home-assistant path: lib/home-assistant
- name: Setup Python - name: Setup Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: 3.13 python-version: 3.13
@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files python script/run-in-env.py pre-commit run --all-files
- name: Commit changes - name: Commit changes
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with: with:
commit-message: "Synchronise Device Classes from Home Assistant" commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@openhomefoundation.org> committer: esphomebot <esphome@openhomefoundation.org>

4
.gitignore vendored
View File

@@ -91,10 +91,6 @@ venv-*/
# mypy # mypy
.mypy_cache/ .mypy_cache/
# nix
/default.nix
/shell.nix
.pioenvs .pioenvs
.piolibdeps .piolibdeps
.pio .pio

View File

@@ -11,7 +11,7 @@ ci:
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.14.11 rev: v0.14.1
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View File

@@ -21,7 +21,6 @@ esphome/components/adc128s102/* @DeerMaximum
esphome/components/addressable_light/* @justfalter esphome/components/addressable_light/* @justfalter
esphome/components/ade7880/* @kpfleming esphome/components/ade7880/* @kpfleming
esphome/components/ade7953/* @angelnu esphome/components/ade7953/* @angelnu
esphome/components/ade7953_base/* @angelnu
esphome/components/ade7953_i2c/* @angelnu esphome/components/ade7953_i2c/* @angelnu
esphome/components/ade7953_spi/* @angelnu esphome/components/ade7953_spi/* @angelnu
esphome/components/ads1118/* @solomondg1 esphome/components/ads1118/* @solomondg1
@@ -42,7 +41,6 @@ esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix esphome/components/anova/* @buxtronix
esphome/components/apds9306/* @aodrenah esphome/components/apds9306/* @aodrenah
esphome/components/api/* @esphome/core esphome/components/api/* @esphome/core
esphome/components/aqi/* @freekode @jasstrong @ximex
esphome/components/as5600/* @ammmze esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr esphome/components/as7341/* @mrgnr
@@ -74,7 +72,6 @@ esphome/components/bl0942/* @dbuezas @dwmw2
esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/ble_nus/* @tomaszduda23 esphome/components/ble_nus/* @tomaszduda23
esphome/components/bluetooth_proxy/* @bdraco @jesserockz esphome/components/bluetooth_proxy/* @bdraco @jesserockz
esphome/components/bm8563/* @abmantis
esphome/components/bme280_base/* @esphome/core esphome/components/bme280_base/* @esphome/core
esphome/components/bme280_spi/* @apbodrov esphome/components/bme280_spi/* @apbodrov
esphome/components/bme680_bsec/* @trvrnrth esphome/components/bme680_bsec/* @trvrnrth
@@ -91,7 +88,6 @@ esphome/components/bmp3xx_spi/* @latonita
esphome/components/bmp581/* @kahrendt esphome/components/bmp581/* @kahrendt
esphome/components/bp1658cj/* @Cossid esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid esphome/components/bp5758d/* @Cossid
esphome/components/bthome_mithermometer/* @nagyrobi
esphome/components/button/* @esphome/core esphome/components/button/* @esphome/core
esphome/components/bytebuffer/* @clydebarrow esphome/components/bytebuffer/* @clydebarrow
esphome/components/camera/* @bdraco @DT-art1 esphome/components/camera/* @bdraco @DT-art1
@@ -99,7 +95,6 @@ esphome/components/camera_encoder/* @DT-art1
esphome/components/canbus/* @danielschramm @mvturnho esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/cap1188/* @mreditor97 esphome/components/cap1188/* @mreditor97
esphome/components/captive_portal/* @esphome/core esphome/components/captive_portal/* @esphome/core
esphome/components/cc1101/* @gabest11 @lygris
esphome/components/ccs811/* @habbie esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @clydebarrow @jesterret esphome/components/ch422g/* @clydebarrow @jesterret
@@ -135,7 +130,7 @@ esphome/components/display_menu_base/* @numo68
esphome/components/dps310/* @kbx81 esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its esphome/components/ds2484/* @mrk-its
esphome/components/dsmr/* @glmnet @PolarGoose @zuidwijk esphome/components/dsmr/* @glmnet @zuidwijk
esphome/components/duty_time/* @dudanov esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M esphome/components/ee895/* @Stock-M
esphome/components/ektf2232/touchscreen/* @jesserockz esphome/components/ektf2232/touchscreen/* @jesserockz
@@ -160,14 +155,12 @@ esphome/components/esp32_ble_tracker/* @bdraco
esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337 esphome/components/esp32_hosted/* @swoboda1337
esphome/components/esp32_hosted/update/* @swoboda1337
esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core esphome/components/esp8266/* @esphome/core
esphome/components/esp_ldo/* @clydebarrow esphome/components/esp_ldo/* @clydebarrow
esphome/components/espnow/* @jesserockz esphome/components/espnow/* @jesserockz
esphome/components/espnow/packet_transport/* @EasilyBoredEngineer
esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat esphome/components/event/* @nohat
esphome/components/exposure_notifications/* @OttoWinter esphome/components/exposure_notifications/* @OttoWinter
@@ -186,14 +179,13 @@ esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval esphome/components/gl_r01_i2c/* @pkejval
esphome/components/globals/* @esphome/core esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz @sebydocky esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb esphome/components/gpio/one_wire/* @ssieb
esphome/components/gps/* @coogle @ximex esphome/components/gps/* @coogle @ximex
esphome/components/graph/* @synco esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson esphome/components/graphical_display_menu/* @MrMDavidson
esphome/components/gree/* @orestismers esphome/components/gree/* @orestismers
esphome/components/gree/switch/* @nagyrobi
esphome/components/grove_gas_mc_v2/* @YorkshireIoT esphome/components/grove_gas_mc_v2/* @YorkshireIoT
esphome/components/grove_tb6612fng/* @max246 esphome/components/grove_tb6612fng/* @max246
esphome/components/growatt_solar/* @leeuwte esphome/components/growatt_solar/* @leeuwte
@@ -208,16 +200,11 @@ esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/light/* @DotNetDann
esphome/components/hbridge/switch/* @dwmw2 esphome/components/hbridge/switch/* @dwmw2
esphome/components/hc8/* @omartijn
esphome/components/hdc2010/* @optimusprimespace @ssieb
esphome/components/he60r/* @clydebarrow esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hlk_fm22x/* @OnFreund
esphome/components/hlw8032/* @rici4kubicek
esphome/components/hm3301/* @freekode esphome/components/hm3301/* @freekode
esphome/components/hmac_md5/* @dwmw2 esphome/components/hmac_md5/* @dwmw2
esphome/components/hmac_sha256/* @dwmw2
esphome/components/homeassistant/* @esphome/core @OttoWinter esphome/components/homeassistant/* @esphome/core @OttoWinter
esphome/components/homeassistant/number/* @landonr esphome/components/homeassistant/number/* @landonr
esphome/components/homeassistant/switch/* @Links2004 esphome/components/homeassistant/switch/* @Links2004
@@ -231,7 +218,6 @@ esphome/components/hte501/* @Stock-M
esphome/components/http_request/ota/* @oarcher esphome/components/http_request/ota/* @oarcher
esphome/components/http_request/update/* @jesserockz esphome/components/http_request/update/* @jesserockz
esphome/components/htu31d/* @betterengineering esphome/components/htu31d/* @betterengineering
esphome/components/hub75/* @stuartparmenter
esphome/components/hydreon_rgxx/* @functionpointer esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/hyt271/* @Philippe12 esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core esphome/components/i2c/* @esphome/core
@@ -249,7 +235,6 @@ esphome/components/ina260/* @mreditor97
esphome/components/ina2xx_base/* @latonita esphome/components/ina2xx_base/* @latonita
esphome/components/ina2xx_i2c/* @latonita esphome/components/ina2xx_i2c/* @latonita
esphome/components/ina2xx_spi/* @latonita esphome/components/ina2xx_spi/* @latonita
esphome/components/infrared/* @kbx81
esphome/components/inkbird_ibsth1_mini/* @fkirill esphome/components/inkbird_ibsth1_mini/* @fkirill
esphome/components/inkplate/* @jesserockz @JosipKuci esphome/components/inkplate/* @jesserockz @JosipKuci
esphome/components/integration/* @OttoWinter esphome/components/integration/* @OttoWinter
@@ -302,7 +287,6 @@ esphome/components/mcp23x17_base/* @jesserockz
esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz
esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp2515/* @danielschramm @mvturnho
esphome/components/mcp3204/* @rsumner esphome/components/mcp3204/* @rsumner
esphome/components/mcp3221/* @philippderdiedas
esphome/components/mcp4461/* @p1ngb4ck esphome/components/mcp4461/* @p1ngb4ck
esphome/components/mcp4728/* @berfenger esphome/components/mcp4728/* @berfenger
esphome/components/mcp47a1/* @jesserockz esphome/components/mcp47a1/* @jesserockz
@@ -312,7 +296,7 @@ esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz esphome/components/media_player/* @jesserockz
esphome/components/micro_wake_word/* @jesserockz @kahrendt esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @edenhaus @jorre05 esphome/components/micronova/* @jorre05
esphome/components/microphone/* @jesserockz @kahrendt esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov esphome/components/midea/* @dudanov
@@ -396,7 +380,6 @@ esphome/components/radon_eye_rd200/* @jeffeb3
esphome/components/rc522/* @glmnet esphome/components/rc522/* @glmnet
esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_i2c/* @glmnet
esphome/components/rc522_spi/* @glmnet esphome/components/rc522_spi/* @glmnet
esphome/components/rd03d/* @jasstrong
esphome/components/resampler/speaker/* @kahrendt esphome/components/resampler/speaker/* @kahrendt
esphome/components/restart/* @esphome/core esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz esphome/components/rf_bridge/* @jesserockz
@@ -408,7 +391,6 @@ esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet
esphome/components/runtime_stats/* @bdraco esphome/components/runtime_stats/* @bdraco
esphome/components/rx8130/* @beormund
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core esphome/components/script/* @esphome/core
@@ -472,7 +454,6 @@ esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81 esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155 esphome/components/st7920/* @marsjan155
esphome/components/statsd/* @Links2004 esphome/components/statsd/* @Links2004
esphome/components/stts22h/* @B48D81EFCC
esphome/components/substitutions/* @esphome/core esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931 esphome/components/sun_gtil2/* @Mat931
@@ -494,10 +475,8 @@ esphome/components/template/datetime/* @rfdarter
esphome/components/template/event/* @nohat esphome/components/template/event/* @nohat
esphome/components/template/fan/* @ssieb esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse esphome/components/text/* @mauritskorse
esphome/components/thermopro_ble/* @sittner
esphome/components/thermostat/* @kbx81 esphome/components/thermostat/* @kbx81
esphome/components/time/* @esphome/core esphome/components/time/* @esphome/core
esphome/components/tinyusb/* @kbx81
esphome/components/tlc5947/* @rnauber esphome/components/tlc5947/* @rnauber
esphome/components/tlc5971/* @IJIJI esphome/components/tlc5971/* @IJIJI
esphome/components/tm1621/* @Philippe12 esphome/components/tm1621/* @Philippe12
@@ -522,7 +501,6 @@ esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra esphome/components/tuya/text_sensor/* @dentra
esphome/components/uart/* @esphome/core esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_transport/* @clydebarrow esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/udp/* @clydebarrow esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ec/* @pvizeli
@@ -530,7 +508,6 @@ esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter esphome/components/ultrasonic/* @OttoWinter
esphome/components/update/* @jesserockz esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon esphome/components/uponor_smatrix/* @kroimon
esphome/components/usb_cdc_acm/* @kbx81
esphome/components/usb_host/* @clydebarrow esphome/components/usb_host/* @clydebarrow
esphome/components/usb_uart/* @clydebarrow esphome/components/usb_uart/* @clydebarrow
esphome/components/valve/* @esphome/core esphome/components/valve/* @esphome/core
@@ -541,7 +518,6 @@ esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz @kahrendt esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher esphome/components/watchdog/* @oarcher
esphome/components/water_heater/* @dhoeben
esphome/components/waveshare_epaper/* @clydebarrow esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server/ota/* @esphome/core esphome/components/web_server/ota/* @esphome/core
esphome/components/web_server_base/* @esphome/core esphome/components/web_server_base/* @esphome/core
@@ -577,6 +553,5 @@ esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23 esphome/components/zephyr/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zigbee/* @tomaszduda23
esphome/components/zio_ultrasonic/* @kahrendt esphome/components/zio_ultrasonic/* @kahrendt
esphome/components/zwave_proxy/* @kbx81 esphome/components/zwave_proxy/* @kbx81

View File

@@ -2,7 +2,7 @@
We welcome contributions to the ESPHome suite of code and documentation! We welcome contributions to the ESPHome suite of code and documentation!
Please read our [contributing guide](https://developers.esphome.io/contributing/code/) if you wish to contribute to the Please read our [contributing guide](https://esphome.io/guides/contributing.html) if you wish to contribute to the
project and be sure to join us on [Discord](https://discord.gg/KhAMKrd). project and be sure to join us on [Discord](https://discord.gg/KhAMKrd).
**See also:** **See also:**

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2026.1.0-dev PROJECT_NUMBER = 2025.11.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a

View File

@@ -1,7 +1,6 @@
include LICENSE include LICENSE
include README.md include README.md
include requirements.txt include requirements.txt
recursive-include esphome *.yaml
recursive-include esphome *.cpp *.h *.tcc *.c recursive-include esphome *.cpp *.h *.tcc *.c
recursive-include esphome *.py.script recursive-include esphome *.py.script
recursive-include esphome LICENSE.txt recursive-include esphome LICENSE.txt

View File

@@ -2,8 +2,8 @@
<a href="https://esphome.io/"> <a href="https://esphome.io/">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://media.esphome.io/logo/logo-text-on-dark.svg"> <source media="(prefers-color-scheme: dark)" srcset="https://esphome.io/_static/logo-text-on-dark.svg", alt="ESPHome Logo">
<img src="https://media.esphome.io/logo/logo-text-on-light.svg" alt="ESPHome Logo"> <img src="https://esphome.io/_static/logo-text-on-light.svg" alt="ESPHome Logo">
</picture> </picture>
</a> </a>

View File

@@ -11,16 +11,6 @@ FROM base-source-${BUILD_TYPE} AS base
RUN git config --system --add safe.directory "*" RUN git config --system --add safe.directory "*"
# Install build tools for Python packages that require compilation
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
RUN if command -v apk > /dev/null; then \
apk add --no-cache build-base; \
else \
apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*; \
fi
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir -U pip uv==0.6.14 RUN pip install --no-cache-dir -U pip uv==0.6.14

View File

@@ -62,43 +62,6 @@ from esphome.util import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Maximum buffer size for serial log reading to prevent unbounded memory growth
SERIAL_BUFFER_MAX_SIZE = 65536
# Special non-component keys that appear in configs
_NON_COMPONENT_KEYS = frozenset(
{
CONF_ESPHOME,
"substitutions",
"packages",
"globals",
"external_components",
"<<",
}
)
def detect_external_components(config: ConfigType) -> set[str]:
"""Detect external/custom components in the configuration.
External components are those that appear in the config but are not
part of ESPHome's built-in components and are not special config keys.
Args:
config: The ESPHome configuration dictionary
Returns:
A set of external component names
"""
from esphome.analyze_memory.helpers import get_esphome_components
builtin_components = get_esphome_components()
return {
key
for key in config
if key not in builtin_components and key not in _NON_COMPONENT_KEYS
}
class ArgsProtocol(Protocol): class ArgsProtocol(Protocol):
device: list[str] | None device: list[str] | None
@@ -210,14 +173,14 @@ def choose_upload_log_host(
if has_mqtt_logging(): if has_mqtt_logging():
resolved.append("MQTT") resolved.append("MQTT")
if has_api() and has_non_ip_address() and has_resolvable_address(): if has_api() and has_non_ip_address():
resolved.extend(_resolve_with_cache(CORE.address, purpose)) resolved.extend(_resolve_with_cache(CORE.address, purpose))
elif purpose == Purpose.UPLOADING: elif purpose == Purpose.UPLOADING:
if has_ota() and has_mqtt_ip_lookup(): if has_ota() and has_mqtt_ip_lookup():
resolved.append("MQTTIP") resolved.append("MQTTIP")
if has_ota() and has_non_ip_address() and has_resolvable_address(): if has_ota() and has_non_ip_address():
resolved.extend(_resolve_with_cache(CORE.address, purpose)) resolved.extend(_resolve_with_cache(CORE.address, purpose))
else: else:
resolved.append(device) resolved.append(device)
@@ -321,17 +284,7 @@ def has_resolvable_address() -> bool:
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address).""" """Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable # Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver # The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
if CORE.address is None: return CORE.address is not None
return False
if has_ip_address():
return True
if has_mdns():
return True
# .local mDNS hostnames are only resolvable if mDNS is enabled
return not CORE.address.endswith(".local")
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str): def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
@@ -434,37 +387,25 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
while tries < 5: while tries < 5:
try: try:
with ser: with ser:
buffer = b""
ser.timeout = 0.1 # 100ms timeout for non-blocking reads
while True: while True:
try: try:
# Read all available data and timestamp it raw = ser.readline()
chunk = ser.read(ser.in_waiting or 1)
if not chunk:
continue
time_ = datetime.now()
milliseconds = time_.microsecond // 1000
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{milliseconds:03}]"
# Add to buffer and process complete lines
# Limit buffer size to prevent unbounded memory growth
# if device sends data without newlines
buffer += chunk
if len(buffer) > SERIAL_BUFFER_MAX_SIZE:
buffer = buffer[-SERIAL_BUFFER_MAX_SIZE:]
while b"\n" in buffer:
raw_line, buffer = buffer.split(b"\n", 1)
line = raw_line.replace(b"\r", b"").decode(
"utf8", "backslashreplace"
)
safe_print(parser.parse_line(line, time_str))
backtrace_state = platformio_api.process_stacktrace(
config, line, backtrace_state=backtrace_state
)
except serial.SerialException: except serial.SerialException:
_LOGGER.error("Serial port closed!") _LOGGER.error("Serial port closed!")
return 0 return 0
line = (
raw.replace(b"\r", b"")
.replace(b"\n", b"")
.decode("utf8", "backslashreplace")
)
time_ = datetime.now()
nanoseconds = time_.microsecond // 1000
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]"
safe_print(parser.parse_line(line, time_str))
backtrace_state = platformio_api.process_stacktrace(
config, line, backtrace_state=backtrace_state
)
except serial.SerialException: except serial.SerialException:
tries += 1 tries += 1
time.sleep(1) time.sleep(1)
@@ -533,49 +474,10 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
rc = platformio_api.run_compile(config, CORE.verbose) rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0: if rc != 0:
return rc return rc
# Check if firmware was rebuilt and emit build_info + create manifest
_check_and_emit_build_info()
idedata = platformio_api.get_idedata(config) idedata = platformio_api.get_idedata(config)
return 0 if idedata is not None else 1 return 0 if idedata is not None else 1
def _check_and_emit_build_info() -> None:
"""Check if firmware was rebuilt and emit build_info."""
import json
firmware_path = CORE.firmware_bin
build_info_json_path = CORE.relative_build_path("build_info.json")
# Check if both files exist
if not firmware_path.exists() or not build_info_json_path.exists():
return
# Check if firmware is newer than build_info (indicating a relink occurred)
if firmware_path.stat().st_mtime <= build_info_json_path.stat().st_mtime:
return
# Read build_info from JSON
try:
with open(build_info_json_path, encoding="utf-8") as f:
build_info = json.load(f)
except (OSError, json.JSONDecodeError) as e:
_LOGGER.debug("Failed to read build_info: %s", e)
return
config_hash = build_info.get("config_hash")
build_time_str = build_info.get("build_time_str")
if config_hash is None or build_time_str is None:
return
# Emit build_info with human-readable time
_LOGGER.info(
"Build Info: config_hash=0x%08x build_time_str=%s", config_hash, build_time_str
)
def upload_using_esptool( def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int config: ConfigType, port: str, file: str, speed: int
) -> str | int: ) -> str | int:
@@ -804,13 +706,7 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = compile_program(args, config) exit_code = compile_program(args, config)
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
if CORE.is_host: _LOGGER.info("Successfully compiled program.")
from esphome.platformio_api import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Successfully compiled program to path '%s'", program_path)
else:
_LOGGER.info("Successfully compiled program.")
return 0 return 0
@@ -860,8 +756,10 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
if CORE.is_host: if CORE.is_host:
from esphome.platformio_api import get_idedata from esphome.platformio_api import get_idedata
program_path = str(get_idedata(config).firmware_elf_path) idedata = get_idedata(config)
_LOGGER.info("Running program from path '%s'", program_path) if idedata is None:
return 1
program_path = idedata.raw["prog_path"]
return run_external_process(program_path) return run_external_process(program_path)
# Get devices, resolving special identifiers like OTA # Get devices, resolving special identifiers like OTA
@@ -994,73 +892,6 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
return 0 return 0
def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
"""Analyze memory usage by component.
This command compiles the configuration and performs memory analysis.
Compilation is fast if sources haven't changed (just relinking).
"""
from esphome import platformio_api
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
# Always compile to ensure fresh data (fast if no changes - just relinks)
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
exit_code = compile_program(args, config)
if exit_code != 0:
return exit_code
_LOGGER.info("Successfully compiled program.")
# Get idedata for analysis
idedata = platformio_api.get_idedata(config)
if idedata is None:
_LOGGER.error("Failed to get IDE data for memory analysis")
return 1
firmware_elf = Path(idedata.firmware_elf_path)
# Extract external components from config
external_components = detect_external_components(config)
_LOGGER.debug("Detected external components: %s", external_components)
# Perform component memory analysis
_LOGGER.info("Analyzing memory usage...")
analyzer = MemoryAnalyzerCLI(
str(firmware_elf),
idedata.objdump_path,
idedata.readelf_path,
external_components,
idedata=idedata,
)
analyzer.analyze()
# Generate and display component report
report = analyzer.generate_report()
print()
print(report)
# Perform RAM strings analysis
_LOGGER.info("Analyzing RAM strings...")
try:
ram_analyzer = RamStringsAnalyzer(
str(firmware_elf),
objdump_path=idedata.objdump_path,
platform=CORE.target_platform,
)
ram_analyzer.analyze()
# Generate and display RAM strings report
ram_report = ram_analyzer.generate_report()
print()
print(ram_report)
except Exception as e: # pylint: disable=broad-except
_LOGGER.warning("RAM strings analysis failed: %s", e)
return 0
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
new_name = args.name new_name = args.name
for c in new_name: for c in new_name:
@@ -1176,7 +1007,6 @@ POST_CONFIG_ACTIONS = {
"idedata": command_idedata, "idedata": command_idedata,
"rename": command_rename, "rename": command_rename,
"discover": command_discover, "discover": command_discover,
"analyze-memory": command_analyze_memory,
} }
SIMPLE_CONFIG_ACTIONS = [ SIMPLE_CONFIG_ACTIONS = [
@@ -1396,7 +1226,7 @@ def parse_args(argv):
"clean-all", help="Clean all build and platform files." "clean-all", help="Clean all build and platform files."
) )
parser_clean_all.add_argument( parser_clean_all.add_argument(
"configuration", help="Your YAML file or configuration directory.", nargs="*" "configuration", help="Your YAML configuration directory.", nargs="*"
) )
parser_dashboard = subparsers.add_parser( parser_dashboard = subparsers.add_parser(
@@ -1462,14 +1292,6 @@ def parse_args(argv):
) )
parser_rename.add_argument("name", help="The new name for the device.", type=str) parser_rename.add_argument("name", help="The new name for the device.", type=str)
parser_analyze_memory = subparsers.add_parser(
"analyze-memory",
help="Analyze memory usage by component.",
)
parser_analyze_memory.add_argument(
"configuration", help="Your YAML configuration file(s).", nargs="+"
)
# Keep backward compatibility with the old command line format of # Keep backward compatibility with the old command line format of
# esphome <config> <command>. # esphome <config> <command>.
# #

View File

@@ -15,20 +15,27 @@ from .const import (
SECTION_TO_ATTR, SECTION_TO_ATTR,
SYMBOL_PATTERNS, SYMBOL_PATTERNS,
) )
from .demangle import batch_demangle
from .helpers import ( from .helpers import (
get_component_class_patterns, get_component_class_patterns,
get_esphome_components, get_esphome_components,
map_section_name, map_section_name,
parse_symbol_line, parse_symbol_line,
) )
from .toolchain import find_tool, run_tool
if TYPE_CHECKING: if TYPE_CHECKING:
from esphome.platformio_api import IDEData from esphome.platformio_api import IDEData
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# GCC global constructor/destructor prefix annotations
_GCC_PREFIX_ANNOTATIONS = {
"_GLOBAL__sub_I_": "global constructor for",
"_GLOBAL__sub_D_": "global destructor for",
}
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
_GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
# C++ runtime patterns for categorization # C++ runtime patterns for categorization
_CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"]) _CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"])
@@ -54,9 +61,6 @@ _NAMESPACE_STD = "std::"
# Type alias for symbol information: (symbol_name, size, component) # Type alias for symbol information: (symbol_name, size, component)
SymbolInfoType = tuple[str, int, str] SymbolInfoType = tuple[str, int, str]
# RAM sections - symbols in these sections consume RAM
RAM_SECTIONS = frozenset([".data", ".bss"])
@dataclass @dataclass
class MemorySection: class MemorySection:
@@ -64,20 +68,7 @@ class MemorySection:
name: str name: str
symbols: list[SymbolInfoType] = field(default_factory=list) symbols: list[SymbolInfoType] = field(default_factory=list)
total_size: int = 0 # Actual section size from ELF headers total_size: int = 0
symbol_size: int = 0 # Sum of symbol sizes (may be less than total_size)
@dataclass
class SDKSymbol:
"""Represents a symbol from an SDK library that's not in the ELF symbol table."""
name: str
size: int
library: str # Name of the .a file (e.g., "libpp.a")
section: str # ".bss" or ".data"
is_local: bool # True if static/local symbol (lowercase in nm output)
demangled: str = "" # Demangled name (populated after analysis)
@dataclass @dataclass
@@ -135,10 +126,6 @@ class MemoryAnalyzer:
self.objdump_path = objdump_path or "objdump" self.objdump_path = objdump_path or "objdump"
self.readelf_path = readelf_path or "readelf" self.readelf_path = readelf_path or "readelf"
self.external_components = external_components or set() self.external_components = external_components or set()
self._idedata = idedata
# Derive nm path from objdump path using shared toolchain utility
self.nm_path = find_tool("nm", self.objdump_path)
self.sections: dict[str, MemorySection] = {} self.sections: dict[str, MemorySection] = {}
self.components: dict[str, ComponentMemory] = defaultdict( self.components: dict[str, ComponentMemory] = defaultdict(
@@ -149,25 +136,15 @@ class MemoryAnalyzer:
self._esphome_core_symbols: list[ self._esphome_core_symbols: list[
tuple[str, str, int] tuple[str, str, int]
] = [] # Track core symbols ] = [] # Track core symbols
# Track symbols for all components: (symbol_name, demangled, size, section) self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict(
self._component_symbols: dict[str, list[tuple[str, str, int, str]]] = (
defaultdict(list)
)
# Track RAM symbols separately for detailed analysis: (symbol_name, demangled, size, section)
self._ram_symbols: dict[str, list[tuple[str, str, int, str]]] = defaultdict(
list list
) ) # Track symbols for all components
# Track ELF symbol names for SDK cross-reference
self._elf_symbol_names: set[str] = set()
# SDK symbols not in ELF (static/local symbols from closed-source libs)
self._sdk_symbols: list[SDKSymbol] = []
def analyze(self) -> dict[str, ComponentMemory]: def analyze(self) -> dict[str, ComponentMemory]:
"""Analyze the ELF file and return component memory usage.""" """Analyze the ELF file and return component memory usage."""
self._parse_sections() self._parse_sections()
self._parse_symbols() self._parse_symbols()
self._categorize_symbols() self._categorize_symbols()
self._analyze_sdk_libraries()
return dict(self.components) return dict(self.components)
def _parse_sections(self) -> None: def _parse_sections(self) -> None:
@@ -221,8 +198,6 @@ class MemoryAnalyzer:
continue continue
self.sections[section].symbols.append((name, size, "")) self.sections[section].symbols.append((name, size, ""))
self.sections[section].symbol_size += size
self._elf_symbol_names.add(name)
seen_addresses.add(address) seen_addresses.add(address)
def _categorize_symbols(self) -> None: def _categorize_symbols(self) -> None:
@@ -266,13 +241,8 @@ class MemoryAnalyzer:
if size > 0: if size > 0:
demangled = self._demangle_symbol(symbol_name) demangled = self._demangle_symbol(symbol_name)
self._component_symbols[component].append( self._component_symbols[component].append(
(symbol_name, demangled, size, section_name) (symbol_name, demangled, size)
) )
# Track RAM symbols separately for detailed RAM analysis
if section_name in RAM_SECTIONS:
self._ram_symbols[component].append(
(symbol_name, demangled, size, section_name)
)
def _identify_component(self, symbol_name: str) -> str: def _identify_component(self, symbol_name: str) -> str:
"""Identify which component a symbol belongs to.""" """Identify which component a symbol belongs to."""
@@ -342,9 +312,168 @@ class MemoryAnalyzer:
if not symbols: if not symbols:
return return
# Try to find the appropriate c++filt for the platform
cppfilt_cmd = "c++filt"
_LOGGER.info("Demangling %d symbols", len(symbols)) _LOGGER.info("Demangling %d symbols", len(symbols))
self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path) _LOGGER.debug("objdump_path = %s", self.objdump_path)
_LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache))
# Check if we have a toolchain-specific c++filt
if self.objdump_path and self.objdump_path != "objdump":
# Replace objdump with c++filt in the path
potential_cppfilt = self.objdump_path.replace("objdump", "c++filt")
_LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt)
if Path(potential_cppfilt).exists():
cppfilt_cmd = potential_cppfilt
_LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd)
else:
_LOGGER.info(
"✗ Toolchain c++filt not found at %s, using system c++filt",
potential_cppfilt,
)
else:
_LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path)
# Strip GCC optimization suffixes and prefixes before demangling
# Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt
# Prefixes like _GLOBAL__sub_I_ need to be removed and tracked
symbols_stripped: list[str] = []
symbols_prefixes: list[str] = [] # Track removed prefixes
for symbol in symbols:
# Remove GCC optimization markers
stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
# Handle GCC global constructor/initializer prefixes
# _GLOBAL__sub_I_<mangled> -> extract <mangled> for demangling
prefix = ""
for gcc_prefix in _GCC_PREFIX_ANNOTATIONS:
if stripped.startswith(gcc_prefix):
prefix = gcc_prefix
stripped = stripped[len(prefix) :]
break
symbols_stripped.append(stripped)
symbols_prefixes.append(prefix)
try:
# Send all symbols to c++filt at once
result = subprocess.run(
[cppfilt_cmd],
input="\n".join(symbols_stripped),
capture_output=True,
text=True,
check=False,
)
except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
# On error, cache originals
_LOGGER.warning("Failed to batch demangle symbols: %s", e)
for symbol in symbols:
self._demangle_cache[symbol] = symbol
return
if result.returncode != 0:
_LOGGER.warning(
"c++filt exited with code %d: %s",
result.returncode,
result.stderr[:200] if result.stderr else "(no error output)",
)
# Cache originals on failure
for symbol in symbols:
self._demangle_cache[symbol] = symbol
return
# Process demangled output
self._process_demangled_output(
symbols, symbols_stripped, symbols_prefixes, result.stdout, cppfilt_cmd
)
def _process_demangled_output(
self,
symbols: list[str],
symbols_stripped: list[str],
symbols_prefixes: list[str],
demangled_output: str,
cppfilt_cmd: str,
) -> None:
"""Process demangled symbol output and populate cache.
Args:
symbols: Original symbol names
symbols_stripped: Stripped symbol names sent to c++filt
symbols_prefixes: Removed prefixes to restore
demangled_output: Output from c++filt
cppfilt_cmd: Path to c++filt command (for logging)
"""
demangled_lines = demangled_output.strip().split("\n")
failed_count = 0
for original, stripped, prefix, demangled in zip(
symbols, symbols_stripped, symbols_prefixes, demangled_lines
):
# Add back any prefix that was removed
demangled = self._restore_symbol_prefix(prefix, stripped, demangled)
# If we stripped a suffix, add it back to the demangled name for clarity
if original != stripped and not prefix:
demangled = self._restore_symbol_suffix(original, demangled)
self._demangle_cache[original] = demangled
# Log symbols that failed to demangle (stayed the same as stripped version)
if stripped == demangled and stripped.startswith("_Z"):
failed_count += 1
if failed_count <= 5: # Only log first 5 failures
_LOGGER.warning("Failed to demangle: %s", original)
if failed_count == 0:
_LOGGER.info("Successfully demangled all %d symbols", len(symbols))
return
_LOGGER.warning(
"Failed to demangle %d/%d symbols using %s",
failed_count,
len(symbols),
cppfilt_cmd,
)
@staticmethod
def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
"""Restore prefix that was removed before demangling.
Args:
prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
stripped: Stripped symbol name
demangled: Demangled symbol name
Returns:
Demangled name with prefix restored/annotated
"""
if not prefix:
return demangled
# Successfully demangled - add descriptive prefix
if demangled != stripped and (
annotation := _GCC_PREFIX_ANNOTATIONS.get(prefix)
):
return f"[{annotation}: {demangled}]"
# Failed to demangle - restore original prefix
return prefix + demangled
@staticmethod
def _restore_symbol_suffix(original: str, demangled: str) -> str:
"""Restore GCC optimization suffix that was removed before demangling.
Args:
original: Original symbol name with suffix
demangled: Demangled symbol name without suffix
Returns:
Demangled name with suffix annotation
"""
if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
return f"{demangled} [{suffix_match.group(1)}]"
return demangled
def _demangle_symbol(self, symbol: str) -> str: def _demangle_symbol(self, symbol: str) -> str:
"""Get demangled C++ symbol name from cache.""" """Get demangled C++ symbol name from cache."""
@@ -366,247 +495,6 @@ class MemoryAnalyzer:
return "Other Core" return "Other Core"
def get_unattributed_ram(self) -> tuple[int, int, int]:
"""Get unattributed RAM sizes (SDK/framework overhead).
Returns:
Tuple of (unattributed_bss, unattributed_data, total_unattributed)
These are bytes in RAM sections that have no corresponding symbols.
"""
bss_section = self.sections.get(".bss")
data_section = self.sections.get(".data")
unattributed_bss = 0
unattributed_data = 0
if bss_section:
unattributed_bss = max(0, bss_section.total_size - bss_section.symbol_size)
if data_section:
unattributed_data = max(
0, data_section.total_size - data_section.symbol_size
)
return unattributed_bss, unattributed_data, unattributed_bss + unattributed_data
def _find_sdk_library_dirs(self) -> list[Path]:
"""Find SDK library directories based on platform.
Returns:
List of paths to SDK library directories containing .a files.
"""
sdk_dirs: list[Path] = []
if self._idedata is None:
return sdk_dirs
# Get the CC path to determine the framework location
cc_path = getattr(self._idedata, "cc_path", None)
if not cc_path:
return sdk_dirs
cc_path = Path(cc_path)
# For ESP8266 Arduino framework
# CC is like: ~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-gcc
# SDK libs are in: ~/.platformio/packages/framework-arduinoespressif8266/tools/sdk/lib/
if "xtensa-lx106" in str(cc_path):
platformio_dir = cc_path.parent.parent.parent
esp8266_sdk = (
platformio_dir
/ "framework-arduinoespressif8266"
/ "tools"
/ "sdk"
/ "lib"
)
if esp8266_sdk.exists():
sdk_dirs.append(esp8266_sdk)
# Also check for NONOSDK subdirectories (closed-source libs)
sdk_dirs.extend(
subdir
for subdir in esp8266_sdk.iterdir()
if subdir.is_dir() and subdir.name.startswith("NONOSDK")
)
# For ESP32 IDF framework
# CC is like: ~/.platformio/packages/toolchain-xtensa-esp-elf/bin/xtensa-esp32-elf-gcc
# or: ~/.platformio/packages/toolchain-riscv32-esp/bin/riscv32-esp-elf-gcc
elif "xtensa-esp" in str(cc_path) or "riscv32-esp" in str(cc_path):
# Detect ESP32 variant from CC path or defines
variant = self._detect_esp32_variant()
if variant:
platformio_dir = cc_path.parent.parent.parent
espidf_dir = platformio_dir / "framework-espidf" / "components"
if espidf_dir.exists():
# Find all directories named after the variant that contain .a files
# This handles various ESP-IDF library layouts:
# - components/*/lib/<variant>/
# - components/*/<variant>/
# - components/*/lib/lib/<variant>/
# - components/*/*/lib_*/<variant>/
sdk_dirs.extend(
variant_dir
for variant_dir in espidf_dir.rglob(variant)
if variant_dir.is_dir() and any(variant_dir.glob("*.a"))
)
return sdk_dirs
def _detect_esp32_variant(self) -> str | None:
"""Detect ESP32 variant from idedata defines.
Returns:
Variant string like 'esp32', 'esp32s2', 'esp32c3', etc. or None.
"""
if self._idedata is None:
return None
defines = getattr(self._idedata, "defines", [])
if not defines:
return None
# ESPHome always adds USE_ESP32_VARIANT_xxx defines
variant_prefix = "USE_ESP32_VARIANT_"
for define in defines:
if define.startswith(variant_prefix):
# Extract variant name and convert to lowercase
# USE_ESP32_VARIANT_ESP32 -> esp32
# USE_ESP32_VARIANT_ESP32S3 -> esp32s3
return define[len(variant_prefix) :].lower()
return None
def _parse_sdk_library(
self, lib_path: Path
) -> tuple[list[tuple[str, int, str, bool]], set[str]]:
"""Parse a single SDK library for symbols.
Args:
lib_path: Path to the .a library file
Returns:
Tuple of:
- List of BSS/DATA symbols: (symbol_name, size, section, is_local)
- Set of global BSS/DATA symbol names (for checking if RAM is linked)
"""
ram_symbols: list[tuple[str, int, str, bool]] = []
global_ram_symbols: set[str] = set()
result = run_tool([self.nm_path, "--size-sort", str(lib_path)], timeout=10)
if result is None:
return ram_symbols, global_ram_symbols
for line in result.stdout.splitlines():
parts = line.split()
if len(parts) < 3:
continue
try:
size = int(parts[0], 16)
sym_type = parts[1]
name = parts[2]
# Only collect BSS (b/B) and DATA (d/D) for RAM analysis
if sym_type in ("b", "B"):
section = ".bss"
is_local = sym_type == "b"
ram_symbols.append((name, size, section, is_local))
# Track global RAM symbols (B/D) for linking check
if sym_type == "B":
global_ram_symbols.add(name)
elif sym_type in ("d", "D"):
section = ".data"
is_local = sym_type == "d"
ram_symbols.append((name, size, section, is_local))
if sym_type == "D":
global_ram_symbols.add(name)
except (ValueError, IndexError):
continue
return ram_symbols, global_ram_symbols
def _analyze_sdk_libraries(self) -> None:
"""Analyze SDK libraries to find symbols not in the ELF.
This finds static/local symbols from closed-source SDK libraries
that consume RAM but don't appear in the final ELF symbol table.
Only includes symbols from libraries that have RAM actually linked
(at least one global BSS/DATA symbol in the ELF).
"""
sdk_dirs = self._find_sdk_library_dirs()
if not sdk_dirs:
_LOGGER.debug("No SDK library directories found")
return
_LOGGER.debug("Analyzing SDK libraries in %d directories", len(sdk_dirs))
# Track seen symbols to avoid duplicates from multiple SDK versions
seen_symbols: set[str] = set()
for sdk_dir in sdk_dirs:
for lib_path in sorted(sdk_dir.glob("*.a")):
lib_name = lib_path.name
ram_symbols, global_ram_symbols = self._parse_sdk_library(lib_path)
# Check if this library's RAM is actually linked by seeing if any
# of its global BSS/DATA symbols appear in the ELF
if not global_ram_symbols & self._elf_symbol_names:
# No RAM from this library is in the ELF - skip it
continue
for name, size, section, is_local in ram_symbols:
# Skip if already in ELF or already seen from another lib
if name in self._elf_symbol_names or name in seen_symbols:
continue
# Only track symbols with non-zero size
if size > 0:
self._sdk_symbols.append(
SDKSymbol(
name=name,
size=size,
library=lib_name,
section=section,
is_local=is_local,
)
)
seen_symbols.add(name)
# Demangle SDK symbols for better readability
if self._sdk_symbols:
sdk_names = [sym.name for sym in self._sdk_symbols]
demangled_map = batch_demangle(sdk_names, objdump_path=self.objdump_path)
for sym in self._sdk_symbols:
sym.demangled = demangled_map.get(sym.name, sym.name)
# Sort by size descending for reporting
self._sdk_symbols.sort(key=lambda s: s.size, reverse=True)
total_sdk_ram = sum(s.size for s in self._sdk_symbols)
_LOGGER.debug(
"Found %d SDK symbols not in ELF, totaling %d bytes",
len(self._sdk_symbols),
total_sdk_ram,
)
def get_sdk_ram_symbols(self) -> list[SDKSymbol]:
"""Get SDK symbols that consume RAM but aren't in the ELF symbol table.
Returns:
List of SDKSymbol objects sorted by size descending.
"""
return self._sdk_symbols
def get_sdk_ram_by_library(self) -> dict[str, list[SDKSymbol]]:
"""Get SDK RAM symbols grouped by library.
Returns:
Dictionary mapping library name to list of symbols.
"""
by_lib: dict[str, list[SDKSymbol]] = defaultdict(list)
for sym in self._sdk_symbols:
by_lib[sym.library].append(sym)
return dict(by_lib)
if __name__ == "__main__": if __name__ == "__main__":
from .cli import main from .cli import main

View File

@@ -1,35 +1,20 @@
"""CLI interface for memory analysis with report generation.""" """CLI interface for memory analysis with report generation."""
from __future__ import annotations
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable
import sys import sys
from typing import TYPE_CHECKING
from . import ( from . import (
_COMPONENT_API, _COMPONENT_API,
_COMPONENT_CORE, _COMPONENT_CORE,
_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_ESPHOME,
_COMPONENT_PREFIX_EXTERNAL, _COMPONENT_PREFIX_EXTERNAL,
RAM_SECTIONS,
MemoryAnalyzer, MemoryAnalyzer,
) )
if TYPE_CHECKING:
from . import ComponentMemory
class MemoryAnalyzerCLI(MemoryAnalyzer): class MemoryAnalyzerCLI(MemoryAnalyzer):
"""Memory analyzer with CLI-specific report generation.""" """Memory analyzer with CLI-specific report generation."""
# Symbol size threshold for detailed analysis
SYMBOL_SIZE_THRESHOLD: int = (
100 # Show symbols larger than this in detailed analysis
)
# Lower threshold for RAM symbols (RAM is more constrained)
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
# Column width constants # Column width constants
COL_COMPONENT: int = 29 COL_COMPONENT: int = 29
COL_FLASH_TEXT: int = 14 COL_FLASH_TEXT: int = 14
@@ -93,60 +78,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
COL_CORE_PERCENT, COL_CORE_PERCENT,
) )
def _add_section_header(self, lines: list[str], title: str) -> None:
"""Add a section header with title centered between separator lines."""
lines.append("")
lines.append("=" * self.TABLE_WIDTH)
lines.append(title.center(self.TABLE_WIDTH))
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
def _add_top_consumers(
self,
lines: list[str],
title: str,
components: list[tuple[str, ComponentMemory]],
get_size: Callable[[ComponentMemory], int],
total: int,
memory_type: str,
limit: int = 25,
) -> None:
"""Add a formatted list of top memory consumers to the report.
Args:
lines: List of report lines to append the output to.
title: Section title to print before the list.
components: Sequence of (name, ComponentMemory) tuples to analyze.
get_size: Callable that takes a ComponentMemory and returns the
size in bytes to use for ranking and display.
total: Total size in bytes for computing percentage usage.
memory_type: Label for the memory region (e.g., "flash" or "RAM").
limit: Maximum number of components to include in the list.
"""
lines.append("")
lines.append(f"{title}:")
for i, (name, mem) in enumerate(components[:limit]):
size = get_size(mem)
if size > 0:
percentage = (size / total * 100) if total > 0 else 0
lines.append(
f"{i + 1}. {name} ({size:,} B) - {percentage:.1f}% of analyzed {memory_type}"
)
def _format_symbol_with_section(
self, demangled: str, size: int, section: str | None = None
) -> str:
"""Format a symbol entry, optionally adding a RAM section label.
If section is one of the RAM sections (.data or .bss), a label like
" [data]" or " [bss]" is appended. For non-RAM sections or when
section is None, no section label is added.
"""
section_label = ""
if section in RAM_SECTIONS:
section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss]
return f"{demangled} ({size:,} B){section_label}"
def generate_report(self, detailed: bool = False) -> str: def generate_report(self, detailed: bool = False) -> str:
"""Generate a formatted memory report.""" """Generate a formatted memory report."""
components = sorted( components = sorted(
@@ -187,70 +118,43 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B" f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B"
) )
# Show unattributed RAM (SDK/framework overhead) # Top consumers
unattributed_bss, unattributed_data, unattributed_total = ( lines.append("")
self.get_unattributed_ram() lines.append("Top Flash Consumers:")
) for i, (name, mem) in enumerate(components[:25]):
if unattributed_total > 0: if mem.flash_total > 0:
lines.append("") percentage = (
lines.append( (mem.flash_total / total_flash * 100) if total_flash > 0 else 0
f"Unattributed RAM: {unattributed_total:,} B (SDK/framework overhead)" )
)
if unattributed_bss > 0 and unattributed_data > 0:
lines.append( lines.append(
f" .bss: {unattributed_bss:,} B | .data: {unattributed_data:,} B" f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash"
) )
# Show SDK symbol breakdown if available lines.append("")
sdk_by_lib = self.get_sdk_ram_by_library() lines.append("Top RAM Consumers:")
if sdk_by_lib:
lines.append("")
lines.append("SDK library breakdown (static symbols not in ELF):")
# Sort libraries by total size
lib_totals = [
(lib, sum(s.size for s in syms), syms)
for lib, syms in sdk_by_lib.items()
]
lib_totals.sort(key=lambda x: x[1], reverse=True)
for lib_name, lib_total, syms in lib_totals:
if lib_total == 0:
continue
lines.append(f" {lib_name}: {lib_total:,} B")
# Show top symbols from this library
for sym in sorted(syms, key=lambda s: s.size, reverse=True)[:3]:
section_label = sym.section.lstrip(".")
# Use demangled name (falls back to original if not demangled)
display_name = sym.demangled or sym.name
if len(display_name) > 50:
display_name = f"{display_name[:47]}..."
lines.append(
f" {sym.size:>6,} B [{section_label}] {display_name}"
)
# Top consumers
self._add_top_consumers(
lines,
"Top Flash Consumers",
components,
lambda m: m.flash_total,
total_flash,
"flash",
)
ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True) ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True)
self._add_top_consumers( for i, (name, mem) in enumerate(ram_components[:25]):
lines, if mem.ram_total > 0:
"Top RAM Consumers", percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0
ram_components, lines.append(
lambda m: m.ram_total, f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM"
total_ram, )
"RAM",
lines.append("")
lines.append(
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
) )
lines.append("=" * self.TABLE_WIDTH)
# Add ESPHome core detailed analysis if there are core symbols # Add ESPHome core detailed analysis if there are core symbols
if self._esphome_core_symbols: if self._esphome_core_symbols:
self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis") lines.append("")
lines.append("=" * self.TABLE_WIDTH)
lines.append(
f"{_COMPONENT_CORE} Detailed Analysis".center(self.TABLE_WIDTH)
)
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
# Group core symbols by subcategory # Group core symbols by subcategory
core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict( core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict(
@@ -287,26 +191,15 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%" f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%"
) )
# All core symbols above threshold # Top 15 largest core symbols
lines.append("") lines.append("")
lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:")
sorted_core_symbols = sorted( sorted_core_symbols = sorted(
self._esphome_core_symbols, key=lambda x: x[2], reverse=True self._esphome_core_symbols, key=lambda x: x[2], reverse=True
) )
large_core_symbols = [
(symbol, demangled, size)
for symbol, demangled, size in sorted_core_symbols
if size > self.SYMBOL_SIZE_THRESHOLD
]
lines.append( for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]):
f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):" lines.append(f"{i + 1}. {demangled} ({size:,} B)")
)
for i, (symbol, demangled, size) in enumerate(large_core_symbols):
# Core symbols only track (symbol, demangled, size) without section info,
# so we don't show section labels here
lines.append(
f"{i + 1}. {self._format_symbol_with_section(demangled, size)}"
)
lines.append("=" * self.TABLE_WIDTH) lines.append("=" * self.TABLE_WIDTH)
@@ -338,22 +231,9 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
api_component = (name, mem) api_component = (name, mem)
break break
# Also include wifi_stack and other important system components if they exist # Combine all components to analyze: top ESPHome + all external + API if not already included
system_components_to_include = [ components_to_analyze = list(top_esphome_components) + list(
# Empty list - we've finished debugging symbol categorization top_external_components
# Add component names here if you need to debug their symbols
]
system_components = [
(name, mem)
for name, mem in components
if name in system_components_to_include
]
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components
components_to_analyze = (
list(top_esphome_components)
+ list(top_external_components)
+ system_components
) )
if api_component and api_component not in components_to_analyze: if api_component and api_component not in components_to_analyze:
components_to_analyze.append(api_component) components_to_analyze.append(api_component)
@@ -362,7 +242,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
for comp_name, comp_mem in components_to_analyze: for comp_name, comp_mem in components_to_analyze:
if not (comp_symbols := self._component_symbols.get(comp_name, [])): if not (comp_symbols := self._component_symbols.get(comp_name, [])):
continue continue
self._add_section_header(lines, f"{comp_name} Detailed Analysis") lines.append("")
lines.append("=" * self.TABLE_WIDTH)
lines.append(f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH))
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
# Sort symbols by size # Sort symbols by size
sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True) sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True)
@@ -371,71 +255,19 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append(f"Total size: {comp_mem.flash_total:,} B")
lines.append("") lines.append("")
# Show all symbols above threshold for better visibility # Show all symbols > 100 bytes for better visibility
large_symbols = [ large_symbols = [
(sym, dem, size, sec) (sym, dem, size) for sym, dem, size in sorted_symbols if size > 100
for sym, dem, size, sec in sorted_symbols
if size > self.SYMBOL_SIZE_THRESHOLD
] ]
lines.append( lines.append(
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):"
) )
for i, (symbol, demangled, size, section) in enumerate(large_symbols): for i, (symbol, demangled, size) in enumerate(large_symbols):
lines.append( lines.append(f"{i + 1}. {demangled} ({size:,} B)")
f"{i + 1}. {self._format_symbol_with_section(demangled, size, section)}"
)
lines.append("=" * self.TABLE_WIDTH) lines.append("=" * self.TABLE_WIDTH)
# Detailed RAM analysis by component (at end, before RAM strings analysis)
self._add_section_header(lines, "RAM Symbol Analysis by Component")
# Show top 15 RAM consumers with their large symbols
for name, mem in ram_components[:15]:
if mem.ram_total == 0:
continue
ram_syms = self._ram_symbols.get(name, [])
if not ram_syms:
continue
# Sort by size descending
sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True)
large_ram_syms = [
s for s in sorted_ram_syms if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD
]
lines.append(f"{name} ({mem.ram_total:,} B total RAM):")
# Show breakdown by section type
data_size = sum(s[2] for s in ram_syms if s[3] == ".data")
bss_size = sum(s[2] for s in ram_syms if s[3] == ".bss")
lines.append(f" .data (initialized): {data_size:,} B")
lines.append(f" .bss (uninitialized): {bss_size:,} B")
if large_ram_syms:
lines.append(
f" Symbols > {self.RAM_SYMBOL_SIZE_THRESHOLD} B ({len(large_ram_syms)}):"
)
for symbol, demangled, size, section in large_ram_syms[:10]:
# Format section label consistently by stripping leading dot
section_label = section.lstrip(".") if section else ""
# Add ellipsis if name is truncated
demangled_display = (
f"{demangled[:70]}..." if len(demangled) > 70 else demangled
)
lines.append(
f" {size:>6,} B [{section_label}] {demangled_display}"
)
if len(large_ram_syms) > 10:
lines.append(f" ... and {len(large_ram_syms) - 10} more")
lines.append("")
lines.append(
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
)
lines.append("=" * self.TABLE_WIDTH)
return "\n".join(lines) return "\n".join(lines)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None: def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:

View File

@@ -7,13 +7,11 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
# Section mapping for ELF file sections # Section mapping for ELF file sections
# Maps standard section names to their various platform-specific variants # Maps standard section names to their various platform-specific variants
# Note: Order matters! More specific patterns (.bss) must come before general ones (.dram)
# because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise
SECTION_MAPPING = { SECTION_MAPPING = {
".text": frozenset([".text", ".iram"]), ".text": frozenset([".text", ".iram"]),
".rodata": frozenset([".rodata"]), ".rodata": frozenset([".rodata"]),
".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss"
".data": frozenset([".data", ".dram"]), ".data": frozenset([".data", ".dram"]),
".bss": frozenset([".bss"]),
} }
# Section to ComponentMemory attribute mapping # Section to ComponentMemory attribute mapping
@@ -90,77 +88,6 @@ SYMBOL_PATTERNS = {
"sys_mbox_new", "sys_mbox_new",
"sys_arch_mbox_tryfetch", "sys_arch_mbox_tryfetch",
], ],
# LibreTiny/Beken BK7231 radio calibration
"bk_radio_cal": [
"bk7011_",
"calibration_main",
"gcali_",
"rwnx_cal",
],
# LibreTiny/Beken WiFi MAC layer
"bk_wifi_mac": [
"rxu_", # RX upper layer
"txu_", # TX upper layer
"txl_", # TX lower layer
"rxl_", # RX lower layer
"scanu_", # Scan unit
"mm_hw_", # MAC management hardware
"mm_bcn", # MAC management beacon
"mm_tim", # MAC management TIM
"mm_check", # MAC management checks
"sm_connect", # Station management
"me_beacon", # Management entity beacon
"me_build", # Management entity build
"hapd_", # Host AP daemon
"chan_pre_", # Channel management
"handle_probe_", # Probe handling
],
# LibreTiny/Beken system control
"bk_system": [
"sctrl_", # System control
"icu_ctrl", # Interrupt control unit
"gdma_ctrl", # DMA control
"mpb_ctrl", # MPB control
"uf2_", # UF2 OTA
"bkreg_", # Beken registers
],
# LibreTiny/Beken BLE stack
"bk_ble": [
"gapc_", # GAP client
"gattc_", # GATT client
"attc_", # ATT client
"attmdb_", # ATT database
"atts_", # ATT server
"l2cc_", # L2CAP
"prf_env", # Profile environment
],
# LibreTiny/Beken scheduler
"bk_scheduler": [
"sch_plan_", # Scheduler plan
"sch_prog_", # Scheduler program
"sch_arb_", # Scheduler arbiter
],
# LibreTiny/Beken DMA descriptors
"bk_dma": [
"rx_payload_desc",
"rx_dma_hdrdesc",
"tx_hw_desc",
"host_event_data",
"host_cmd_data",
],
# ARM EABI compiler runtime (LibreTiny uses ARM Cortex-M)
"arm_runtime": [
"__aeabi_",
"__adddf3",
"__subdf3",
"__muldf3",
"__divdf3",
"__addsf3",
"__subsf3",
"__mulsf3",
"__divsf3",
"__gnu_unwind",
],
"xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"], "xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"],
"heap": ["heap_", "multi_heap"], "heap": ["heap_", "multi_heap"],
"spi_flash": ["spi_flash"], "spi_flash": ["spi_flash"],
@@ -200,39 +127,40 @@ SYMBOL_PATTERNS = {
"tryget_socket_unconn", "tryget_socket_unconn",
"cs_create_ctrl_sock", "cs_create_ctrl_sock",
"netbuf_alloc", "netbuf_alloc",
"tcp_", # TCP protocol functions
"udp_", # UDP protocol functions
"lwip_", # LwIP stack functions
"eagle_lwip", # ESP-specific LwIP functions
"new_linkoutput", # Link output function
"acd_", # Address Conflict Detection (ACD)
"eth_", # Ethernet functions
"mac_enable_bb", # MAC baseband enable
"reassemble_and_dispatch", # Packet reassembly
], ],
# dhcp must come before libc to avoid "dhcp_select" matching "select" pattern
"dhcp": ["dhcp", "handle_dhcp"],
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
# Order matters! More specific categories must come before general ones. "wifi_stack": [
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern "ieee80211",
"mdns_lib": ["mdns"], "hostap",
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols "sta_",
"memory_mgmt": [ "ap_",
"mem_", "scan_",
"memory_", "wifi_",
"tlsf_", "wpa_",
"memp_", "wps_",
"pbuf_", "esp_wifi",
"pbuf_alloc", "cnx_",
"pbuf_copy_partial_pbuf", "wpa3_",
"esp_mmu_map", "sae_",
"mmu_hal_", "wDev_",
"s_do_mapping", # Memory mapping function, not WiFi "ic_",
"hash_map_", # Hash map data structure "mac_",
"umm_assimilate", # UMM malloc assimilation "esf_buf",
"gWpaSm",
"sm_WPA",
"eapol_",
"owe_",
"wifiLowLevelInit",
"s_do_mapping",
"gScanStruct",
"ppSearchTxframe",
"ppMapWaitTxq",
"ppFillAMPDUBar",
"ppCheckTxConnTrafficIdle",
"ppCalTkipMic",
], ],
# Bluetooth categories must come BEFORE wifi_stack to avoid misclassification "bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"],
# Many BLE symbols contain patterns like "ble_" that would otherwise match wifi patterns "wifi_bt_coex": ["coex"],
"bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"], "bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"],
"bluedroid_bt": [ "bluedroid_bt": [
"bluedroid", "bluedroid",
@@ -279,61 +207,6 @@ SYMBOL_PATTERNS = {
"copy_extra_byte_in_db", "copy_extra_byte_in_db",
"parse_read_local_supported_commands_response", "parse_read_local_supported_commands_response",
], ],
"bluetooth": [
"bt_",
"_ble_", # More specific than "ble_" to avoid matching "able_", "enable_", "disable_"
"l2c_",
"l2ble_", # L2CAP for BLE
"gatt_",
"gap_",
"hci_",
"btsnd_hcic_", # Bluetooth HCI command send functions
"BT_init",
"BT_tx_", # Bluetooth transmit functions
"esp_ble_", # Catch esp_ble_* functions
],
"bluetooth_ll": [
"llm_", # Link layer manager
"llc_", # Link layer control
"lld_", # Link layer driver
"ld_acl_", # Link layer ACL (Asynchronous Connection-Oriented)
"llcp_", # Link layer control protocol
"lmp_", # Link manager protocol
],
"wifi_bt_coex": ["coex"],
"wifi_stack": [
"ieee80211",
"hostap",
"sta_",
"wifi_ap_", # More specific than "ap_" to avoid matching "cap_", "map_"
"wifi_scan_", # More specific than "scan_" to avoid matching "_scan_" in other contexts
"wifi_",
"wpa_",
"wps_",
"esp_wifi",
"cnx_",
"wpa3_",
"sae_",
"wDev_",
"ic_mac_", # More specific than "mac_" to avoid matching emac_
"esf_buf",
"gWpaSm",
"sm_WPA",
"eapol_",
"owe_",
"wifiLowLevelInit",
# Removed "s_do_mapping" - this is memory management, not WiFi
"gScanStruct",
"ppSearchTxframe",
"ppMapWaitTxq",
"ppFillAMPDUBar",
"ppCheckTxConnTrafficIdle",
"ppCalTkipMic",
"phy_force_wifi",
"phy_unforce_wifi",
"write_wifi_chan",
"wifi_track_pll",
],
"crypto_math": [ "crypto_math": [
"ecp_", "ecp_",
"bignum_", "bignum_",
@@ -358,36 +231,13 @@ SYMBOL_PATTERNS = {
"p_256_init_curve", "p_256_init_curve",
"shift_sub_rows", "shift_sub_rows",
"rshift", "rshift",
"rijndaelEncrypt", # AES Rijndael encryption
],
# System and Arduino core functions must come before libc
"esp_system": [
"system_", # ESP system functions
"postmortem_", # Postmortem reporting
],
"arduino_core": [
"pinMode",
"resetPins",
"millis",
"micros",
"delay(", # More specific - Arduino delay function with parenthesis
"delayMicroseconds",
"digitalWrite",
"digitalRead",
],
"sntp": ["sntp_", "sntp_recv"],
"scheduler": [
"run_scheduled_",
"compute_scheduled_",
"event_TaskQueue",
], ],
"hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"], "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"],
"libc": [ "libc": [
"printf", "printf",
"scanf", "scanf",
"malloc", "malloc",
"_free", # More specific than "free" to match _free, __free_r, etc. but not arbitrary "free" substring "free",
"umm_free", # UMM malloc free function
"memcpy", "memcpy",
"memset", "memset",
"strcpy", "strcpy",
@@ -409,7 +259,7 @@ SYMBOL_PATTERNS = {
"_setenv_r", "_setenv_r",
"_tzset_unlocked_r", "_tzset_unlocked_r",
"__tzcalc_limits", "__tzcalc_limits",
"_select", # More specific than "select" to avoid matching "dhcp_select", etc. "select",
"scalbnf", "scalbnf",
"strtof", "strtof",
"strtof_l", "strtof_l",
@@ -466,24 +316,8 @@ SYMBOL_PATTERNS = {
"CSWTCH$", "CSWTCH$",
"dst$", "dst$",
"sulp", "sulp",
"_strtol_l", # String to long with locale
"__cvt", # Convert
"__utoa", # Unsigned to ASCII
"__global_locale", # Global locale
"_ctype_", # Character type
"impure_data", # Impure data
],
"string_ops": [
"strcmp",
"strncmp",
"strchr",
"strstr",
"strtok",
"strdup",
"strncasecmp_P", # String compare (case insensitive, from program memory)
"strnlen_P", # String length (from program memory)
"strncat_P", # String concatenate (from program memory)
], ],
"string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"],
"memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"],
"file_io": [ "file_io": [
"fread", "fread",
@@ -504,26 +338,10 @@ SYMBOL_PATTERNS = {
"vsscanf", "vsscanf",
], ],
"cpp_anonymous": ["_GLOBAL__N_", "n$"], "cpp_anonymous": ["_GLOBAL__N_", "n$"],
# Plain C patterns only - C++ symbols will be categorized via DEMANGLED_PATTERNS "cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"],
"nvs": ["nvs_"], # Plain C NVS functions "exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"],
"ota": ["ota_", "OTA", "esp_ota", "app_desc"],
# cpp_runtime: Removed _ZN, _ZL to let DEMANGLED_PATTERNS categorize C++ symbols properly
# Only keep patterns that are truly runtime-specific and not categorizable by namespace
"cpp_runtime": ["__cxx", "_ZSt", "__gxx_personality", "_Z16"],
"exception_handling": [
"__cxa_",
"_Unwind_",
"__gcc_personality",
"uw_frame_state",
"search_object", # Search for exception handling object
"get_cie_encoding", # Get CIE encoding
"add_fdes", # Add frame description entries
"fde_unencoded_compare", # Compare FDEs
"fde_mixed_encoding_compare", # Compare mixed encoding FDEs
"frame_downheap", # Frame heap operations
"frame_heapsort", # Frame heap sorting
],
"static_init": ["_GLOBAL__sub_I_"], "static_init": ["_GLOBAL__sub_I_"],
"mdns_lib": ["mdns"],
"phy_radio": [ "phy_radio": [
"phy_", "phy_",
"rf_", "rf_",
@@ -576,47 +394,10 @@ SYMBOL_PATTERNS = {
"txcal_debuge_mode", "txcal_debuge_mode",
"ant_wifitx_cfg", "ant_wifitx_cfg",
"reg_init_begin", "reg_init_begin",
"tx_cap_init", # TX capacitance init
"ram_set_txcap", # RAM TX capacitance setting
"tx_atten_", # TX attenuation
"txiq_", # TX I/Q calibration
"ram_cal_", # RAM calibration
"ram_rxiq_", # RAM RX I/Q
"readvdd33", # Read VDD33
"test_tout", # Test timeout
"tsen_meas", # Temperature sensor measurement
"bbpll_cal", # Baseband PLL calibration
"set_cal_", # Set calibration
"set_rfanagain_", # Set RF analog gain
"set_txdc_", # Set TX DC
"get_vdd33_", # Get VDD33
"gen_rx_gain_table", # Generate RX gain table
"ram_ana_inf_gating_en", # RAM analog interface gating enable
"tx_cont_en", # TX continuous enable
"tx_delay_cfg", # TX delay configuration
"tx_gain_table_set", # TX gain table set
"check_and_reset_hw_deadlock", # Hardware deadlock check
"s_config", # System/hardware config
"chan14_mic_cfg", # Channel 14 MIC config
],
"wifi_phy_pp": [
"pp_",
"ppT",
"ppR",
"ppP",
"ppInstall",
"ppCalTxAMPDULength",
"ppCheckTx", # Packet processor TX check
"ppCal", # Packet processor calibration
"HdlAllBuffedEb", # Handle buffered EB
], ],
"wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"],
"wifi_lmac": ["lmac"], "wifi_lmac": ["lmac"],
"wifi_device": [ "wifi_device": ["wdev", "wDev_"],
"wdev",
"wDev_",
"ic_set_sta", # Set station mode
"ic_set_vif", # Set virtual interface
],
"power_mgmt": [ "power_mgmt": [
"pm_", "pm_",
"sleep", "sleep",
@@ -625,7 +406,15 @@ SYMBOL_PATTERNS = {
"deep_sleep", "deep_sleep",
"power_down", "power_down",
"g_pm", "g_pm",
"pmc", # Power Management Controller ],
"memory_mgmt": [
"mem_",
"memory_",
"tlsf_",
"memp_",
"pbuf_",
"pbuf_alloc",
"pbuf_copy_partial_pbuf",
], ],
"hal_layer": ["hal_"], "hal_layer": ["hal_"],
"clock_mgmt": [ "clock_mgmt": [
@@ -650,6 +439,7 @@ SYMBOL_PATTERNS = {
"error_handling": ["panic", "abort", "assert", "error_", "fault"], "error_handling": ["panic", "abort", "assert", "error_", "fault"],
"authentication": ["auth"], "authentication": ["auth"],
"ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"], "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"],
"dhcp": ["dhcp", "handle_dhcp"],
"ethernet_phy": [ "ethernet_phy": [
"emac_", "emac_",
"eth_phy_", "eth_phy_",
@@ -828,15 +618,7 @@ SYMBOL_PATTERNS = {
"ampdu_dispatch_upto", "ampdu_dispatch_upto",
], ],
"ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"], "ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"],
"rate_control": [ "rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"],
"rssi_margin",
"rcGetSched",
"get_rate_fcc_index",
"rcGetRate", # Get rate
"rc_get_", # Rate control getters
"rc_set_", # Rate control setters
"rc_enable_", # Rate control enable functions
],
"nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"], "nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"],
"channel_mgmt": ["chm_init", "chm_set_current_channel"], "channel_mgmt": ["chm_init", "chm_set_current_channel"],
"trace": ["trc_init", "trc_onAmpduOp"], "trace": ["trc_init", "trc_onAmpduOp"],
@@ -855,22 +637,7 @@ SYMBOL_PATTERNS = {
"math_internal": ["__mdiff", "__lshift", "__mprec_tens", "quorem"], "math_internal": ["__mdiff", "__lshift", "__mprec_tens", "quorem"],
"character_class": ["__chclass"], "character_class": ["__chclass"],
"camellia": ["camellia_", "camellia_feistel"], "camellia": ["camellia_", "camellia_feistel"],
"crypto_tables": [ "crypto_tables": ["FSb", "FSb2", "FSb3", "FSb4"],
"FSb",
"FSb2",
"FSb3",
"FSb4",
"Te0", # AES encryption table
"Td0", # AES decryption table
"crc32_table", # CRC32 lookup table
"crc_tab", # CRC lookup table
],
"crypto_hash": [
"SHA1Transform", # SHA1 hash function
"MD5Transform", # MD5 hash function
"SHA256",
"SHA512",
],
"event_buffer": ["g_eb_list_desc", "eb_space"], "event_buffer": ["g_eb_list_desc", "eb_space"],
"base_node": ["base_node_", "base_node_add_handler"], "base_node": ["base_node_", "base_node_add_handler"],
"file_descriptor": ["s_fd_table"], "file_descriptor": ["s_fd_table"],
@@ -1032,18 +799,31 @@ SYMBOL_PATTERNS = {
"supports_interlaced_inquiry_scan", "supports_interlaced_inquiry_scan",
"supports_reading_remote_extended_features", "supports_reading_remote_extended_features",
], ],
"bluetooth_ll": [
"lld_pdu_",
"ld_acl_",
"lld_stop_ind_handler",
"lld_evt_winsize_change",
"config_lld_evt_funcs_reset",
"config_lld_funcs_reset",
"config_llm_funcs_reset",
"llm_set_long_adv_data",
"lld_retry_tx_prog",
"llc_link_sup_to_ind_handler",
"config_llc_funcs_reset",
"lld_evt_rxwin_compute",
"config_btdm_funcs_reset",
"config_ea_funcs_reset",
"llc_defalut_state_tab_reset",
"config_rwip_funcs_reset",
"ke_lmp_rx_flooding_detect",
],
} }
# Demangled patterns: patterns found in demangled C++ names # Demangled patterns: patterns found in demangled C++ names
DEMANGLED_PATTERNS = { DEMANGLED_PATTERNS = {
"gpio_driver": ["GPIO"], "gpio_driver": ["GPIO"],
"uart_driver": ["UART"], "uart_driver": ["UART"],
# mdns_lib must come before network_stack to avoid "udp" matching "_udpReadBuffer" in MDNSResponder
"mdns_lib": [
"MDNSResponder",
"MDNSImplementation",
"MDNS",
],
"network_stack": [ "network_stack": [
"lwip", "lwip",
"tcp", "tcp",
@@ -1056,24 +836,6 @@ DEMANGLED_PATTERNS = {
"ethernet", "ethernet",
"ppp", "ppp",
"slip", "slip",
"UdpContext", # UDP context class
"DhcpServer", # DHCP server class
],
"arduino_core": [
"String::", # Arduino String class
"Print::", # Arduino Print class
"HardwareSerial::", # Serial class
"IPAddress::", # IP address class
"EspClass::", # ESP class
"experimental::_SPI", # Experimental SPI
],
"ota": [
"UpdaterClass",
"Updater::",
],
"wifi": [
"ESP8266WiFi",
"WiFi::",
], ],
"wifi_stack": ["NetworkInterface"], "wifi_stack": ["NetworkInterface"],
"nimble_bt": [ "nimble_bt": [
@@ -1092,6 +854,7 @@ DEMANGLED_PATTERNS = {
"rtti": ["__type_info", "__class_type_info"], "rtti": ["__type_info", "__class_type_info"],
"web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"], "web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"],
"async_tcp": ["AsyncClient", "AsyncServer"], "async_tcp": ["AsyncClient", "AsyncServer"],
"mdns_lib": ["mdns"],
"json_lib": [ "json_lib": [
"ArduinoJson", "ArduinoJson",
"JsonDocument", "JsonDocument",

View File

@@ -1,182 +0,0 @@
"""Symbol demangling utilities for memory analysis.
This module provides functions for demangling C++ symbol names using c++filt.
"""
from __future__ import annotations
import logging
import re
import subprocess
from .toolchain import find_tool
_LOGGER = logging.getLogger(__name__)
# GCC global constructor/destructor prefix annotations
GCC_PREFIX_ANNOTATIONS = {
"_GLOBAL__sub_I_": "global constructor for",
"_GLOBAL__sub_D_": "global destructor for",
}
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
def _strip_gcc_annotations(symbol: str) -> tuple[str, str]:
"""Strip GCC optimization suffixes and prefixes from a symbol.
Args:
symbol: The mangled symbol name
Returns:
Tuple of (stripped_symbol, removed_prefix)
"""
# Remove GCC optimization markers
stripped = GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
# Handle GCC global constructor/initializer prefixes
prefix = ""
for gcc_prefix in GCC_PREFIX_ANNOTATIONS:
if stripped.startswith(gcc_prefix):
prefix = gcc_prefix
stripped = stripped[len(prefix) :]
break
return stripped, prefix
def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
"""Restore prefix that was removed before demangling.
Args:
prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
stripped: Stripped symbol name
demangled: Demangled symbol name
Returns:
Demangled name with prefix restored/annotated
"""
if not prefix:
return demangled
# Successfully demangled - add descriptive prefix
if demangled != stripped and (annotation := GCC_PREFIX_ANNOTATIONS.get(prefix)):
return f"[{annotation}: {demangled}]"
# Failed to demangle - restore original prefix
return prefix + demangled
def _restore_symbol_suffix(original: str, demangled: str) -> str:
"""Restore GCC optimization suffix that was removed before demangling.
Args:
original: Original symbol name with suffix
demangled: Demangled symbol name without suffix
Returns:
Demangled name with suffix annotation
"""
if suffix_match := GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
return f"{demangled} [{suffix_match.group(1)}]"
return demangled
def batch_demangle(
symbols: list[str],
cppfilt_path: str | None = None,
objdump_path: str | None = None,
) -> dict[str, str]:
"""Batch demangle C++ symbol names.
Args:
symbols: List of symbol names to demangle
cppfilt_path: Path to c++filt binary (auto-detected if not provided)
objdump_path: Path to objdump binary to derive c++filt path from
Returns:
Dictionary mapping original symbol names to demangled names
"""
cache: dict[str, str] = {}
if not symbols:
return cache
# Find c++filt tool
cppfilt_cmd = cppfilt_path or find_tool("c++filt", objdump_path)
if not cppfilt_cmd:
_LOGGER.warning("Could not find c++filt, symbols will not be demangled")
return {s: s for s in symbols}
_LOGGER.debug("Demangling %d symbols using %s", len(symbols), cppfilt_cmd)
# Strip GCC optimization suffixes and prefixes before demangling
symbols_stripped: list[str] = []
symbols_prefixes: list[str] = []
for symbol in symbols:
stripped, prefix = _strip_gcc_annotations(symbol)
symbols_stripped.append(stripped)
symbols_prefixes.append(prefix)
try:
result = subprocess.run(
[cppfilt_cmd],
input="\n".join(symbols_stripped),
capture_output=True,
text=True,
check=False,
)
except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
_LOGGER.warning("Failed to batch demangle symbols: %s", e)
return {s: s for s in symbols}
if result.returncode != 0:
_LOGGER.warning(
"c++filt exited with code %d: %s",
result.returncode,
result.stderr[:200] if result.stderr else "(no error output)",
)
return {s: s for s in symbols}
# Process demangled output
demangled_lines = result.stdout.strip().split("\n")
# Check for output length mismatch
if len(demangled_lines) != len(symbols):
_LOGGER.warning(
"c++filt output mismatch: expected %d lines, got %d",
len(symbols),
len(demangled_lines),
)
return {s: s for s in symbols}
failed_count = 0
for original, stripped, prefix, demangled in zip(
symbols, symbols_stripped, symbols_prefixes, demangled_lines
):
# Add back any prefix that was removed
demangled = _restore_symbol_prefix(prefix, stripped, demangled)
# If we stripped a suffix, add it back to the demangled name for clarity
if original != stripped and not prefix:
demangled = _restore_symbol_suffix(original, demangled)
cache[original] = demangled
# Count symbols that failed to demangle
if stripped == demangled and stripped.startswith("_Z"):
failed_count += 1
if failed_count <= 5:
_LOGGER.debug("Failed to demangle: %s", original)
if failed_count > 0:
_LOGGER.debug(
"Failed to demangle %d/%d symbols using %s",
failed_count,
len(symbols),
cppfilt_cmd,
)
return cache

View File

@@ -1,493 +0,0 @@
"""Analyzer for RAM-stored strings in ESP8266/ESP32 firmware ELF files.
This module identifies strings that are stored in RAM sections (.data, .bss, .rodata)
rather than in flash sections (.irom0.text, .irom.text), which is important for
memory-constrained platforms like ESP8266.
"""
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
import logging
from pathlib import Path
import re
import subprocess
from .demangle import batch_demangle
from .toolchain import find_tool
_LOGGER = logging.getLogger(__name__)
# ESP8266: .rodata is in RAM (DRAM), not flash
# ESP32: .rodata is in flash, mapped to data bus
ESP8266_RAM_SECTIONS = frozenset([".data", ".rodata", ".bss"])
ESP8266_FLASH_SECTIONS = frozenset([".irom0.text", ".irom.text", ".text"])
# ESP32: .rodata is memory-mapped from flash
ESP32_RAM_SECTIONS = frozenset([".data", ".bss", ".dram0.data", ".dram0.bss"])
ESP32_FLASH_SECTIONS = frozenset([".text", ".rodata", ".flash.text", ".flash.rodata"])
# nm symbol types for data symbols (D=global data, d=local data, R=rodata, B=bss)
DATA_SYMBOL_TYPES = frozenset(["D", "d", "R", "r", "B", "b"])
@dataclass
class SectionInfo:
"""Information about an ELF section."""
name: str
address: int
size: int
@dataclass
class RamString:
"""A string found in RAM."""
section: str
address: int
content: str
@property
def size(self) -> int:
"""Size in bytes including null terminator."""
return len(self.content) + 1
@dataclass
class RamSymbol:
"""A symbol found in RAM."""
name: str
sym_type: str
address: int
size: int
section: str
demangled: str = "" # Demangled name, set after batch demangling
class RamStringsAnalyzer:
"""Analyzes ELF files to find strings stored in RAM."""
def __init__(
self,
elf_path: str,
objdump_path: str | None = None,
min_length: int = 8,
platform: str = "esp32",
) -> None:
"""Initialize the RAM strings analyzer.
Args:
elf_path: Path to the ELF file to analyze
objdump_path: Path to objdump binary (used to find other tools)
min_length: Minimum string length to report (default: 8)
platform: Platform name ("esp8266", "esp32", etc.) for section mapping
"""
self.elf_path = Path(elf_path)
if not self.elf_path.exists():
raise FileNotFoundError(f"ELF file not found: {elf_path}")
self.objdump_path = objdump_path
self.min_length = min_length
self.platform = platform
# Set RAM/flash sections based on platform
if self.platform == "esp8266":
self.ram_sections = ESP8266_RAM_SECTIONS
self.flash_sections = ESP8266_FLASH_SECTIONS
else:
# ESP32 and other platforms
self.ram_sections = ESP32_RAM_SECTIONS
self.flash_sections = ESP32_FLASH_SECTIONS
self.sections: dict[str, SectionInfo] = {}
self.ram_strings: list[RamString] = []
self.ram_symbols: list[RamSymbol] = []
def _run_command(self, cmd: list[str]) -> str:
"""Run a command and return its output."""
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return result.stdout
except subprocess.CalledProcessError as e:
_LOGGER.debug("Command failed: %s - %s", " ".join(cmd), e.stderr)
raise
except FileNotFoundError:
_LOGGER.warning("Command not found: %s", cmd[0])
raise
def analyze(self) -> None:
"""Perform the full RAM analysis."""
self._parse_sections()
self._extract_strings()
self._analyze_symbols()
self._demangle_symbols()
def _parse_sections(self) -> None:
"""Parse section headers from ELF file."""
objdump = find_tool("objdump", self.objdump_path)
if not objdump:
_LOGGER.error("Could not find objdump command")
return
try:
output = self._run_command([objdump, "-h", str(self.elf_path)])
except (subprocess.CalledProcessError, FileNotFoundError):
return
# Parse section headers
# Format: Idx Name Size VMA LMA File off Algn
section_pattern = re.compile(
r"^\s*\d+\s+(\S+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)"
)
for line in output.split("\n"):
if match := section_pattern.match(line):
name = match.group(1)
size = int(match.group(2), 16)
vma = int(match.group(3), 16)
self.sections[name] = SectionInfo(name, vma, size)
def _extract_strings(self) -> None:
"""Extract strings from RAM sections."""
objdump = find_tool("objdump", self.objdump_path)
if not objdump:
return
for section_name in self.ram_sections:
if section_name not in self.sections:
continue
try:
output = self._run_command(
[objdump, "-s", "-j", section_name, str(self.elf_path)]
)
except subprocess.CalledProcessError:
# Section may exist but have no content (e.g., .bss)
continue
except FileNotFoundError:
continue
strings = self._parse_hex_dump(output, section_name)
self.ram_strings.extend(strings)
def _parse_hex_dump(self, output: str, section_name: str) -> list[RamString]:
"""Parse hex dump output to extract strings.
Args:
output: Output from objdump -s
section_name: Name of the section being parsed
Returns:
List of RamString objects
"""
strings: list[RamString] = []
current_string = bytearray()
string_start_addr = 0
for line in output.split("\n"):
# Lines look like: " 3ffef8a0 00000000 00000000 00000000 00000000 ................"
match = re.match(r"^\s+([0-9a-fA-F]+)\s+((?:[0-9a-fA-F]{2,8}\s*)+)", line)
if not match:
continue
addr = int(match.group(1), 16)
hex_data = match.group(2).strip()
# Convert hex to bytes
hex_bytes = hex_data.split()
byte_offset = 0
for hex_chunk in hex_bytes:
# Handle both byte-by-byte and word formats
for i in range(0, len(hex_chunk), 2):
byte_val = int(hex_chunk[i : i + 2], 16)
if 0x20 <= byte_val <= 0x7E: # Printable ASCII
if not current_string:
string_start_addr = addr + byte_offset
current_string.append(byte_val)
else:
if byte_val == 0 and len(current_string) >= self.min_length:
# Found null terminator
strings.append(
RamString(
section=section_name,
address=string_start_addr,
content=current_string.decode(
"ascii", errors="ignore"
),
)
)
current_string = bytearray()
byte_offset += 1
return strings
def _analyze_symbols(self) -> None:
"""Analyze symbols in RAM sections."""
nm = find_tool("nm", self.objdump_path)
if not nm:
return
try:
output = self._run_command([nm, "-S", "--size-sort", str(self.elf_path)])
except (subprocess.CalledProcessError, FileNotFoundError):
return
for line in output.split("\n"):
parts = line.split()
if len(parts) < 4:
continue
try:
addr = int(parts[0], 16)
size = int(parts[1], 16) if parts[1] != "?" else 0
except ValueError:
continue
sym_type = parts[2]
name = " ".join(parts[3:])
# Filter for data symbols
if sym_type not in DATA_SYMBOL_TYPES:
continue
# Check if symbol is in a RAM section
for section_name in self.ram_sections:
if section_name not in self.sections:
continue
section = self.sections[section_name]
if section.address <= addr < section.address + section.size:
self.ram_symbols.append(
RamSymbol(
name=name,
sym_type=sym_type,
address=addr,
size=size,
section=section_name,
)
)
break
def _demangle_symbols(self) -> None:
"""Batch demangle all RAM symbol names."""
if not self.ram_symbols:
return
# Collect all symbol names and demangle them
symbol_names = [s.name for s in self.ram_symbols]
demangle_cache = batch_demangle(symbol_names, objdump_path=self.objdump_path)
# Assign demangled names to symbols
for symbol in self.ram_symbols:
symbol.demangled = demangle_cache.get(symbol.name, symbol.name)
def _get_sections_size(self, section_names: frozenset[str]) -> int:
"""Get total size of specified sections."""
return sum(
section.size
for name, section in self.sections.items()
if name in section_names
)
def get_total_ram_usage(self) -> int:
"""Get total RAM usage from RAM sections."""
return self._get_sections_size(self.ram_sections)
def get_total_flash_usage(self) -> int:
"""Get total flash usage from flash sections."""
return self._get_sections_size(self.flash_sections)
def get_total_string_bytes(self) -> int:
"""Get total bytes used by strings in RAM."""
return sum(s.size for s in self.ram_strings)
def get_repeated_strings(self) -> list[tuple[str, int]]:
"""Find strings that appear multiple times.
Returns:
List of (string, count) tuples sorted by potential savings
"""
string_counts: dict[str, int] = defaultdict(int)
for ram_string in self.ram_strings:
string_counts[ram_string.content] += 1
return sorted(
[(s, c) for s, c in string_counts.items() if c > 1],
key=lambda x: x[1] * (len(x[0]) + 1),
reverse=True,
)
def get_long_strings(self, min_len: int = 20) -> list[RamString]:
"""Get strings longer than the specified length.
Args:
min_len: Minimum string length
Returns:
List of RamString objects sorted by length
"""
return sorted(
[s for s in self.ram_strings if len(s.content) >= min_len],
key=lambda x: len(x.content),
reverse=True,
)
def get_largest_symbols(self, min_size: int = 100) -> list[RamSymbol]:
"""Get RAM symbols larger than the specified size.
Args:
min_size: Minimum symbol size in bytes
Returns:
List of RamSymbol objects sorted by size
"""
return sorted(
[s for s in self.ram_symbols if s.size >= min_size],
key=lambda x: x.size,
reverse=True,
)
def generate_report(self, show_all_sections: bool = False) -> str:
"""Generate a formatted RAM strings analysis report.
Args:
show_all_sections: If True, show all sections, not just RAM
Returns:
Formatted report string
"""
lines: list[str] = []
table_width = 80
lines.append("=" * table_width)
lines.append(
f"RAM Strings Analysis ({self.platform.upper()})".center(table_width)
)
lines.append("=" * table_width)
lines.append("")
# Section Analysis
lines.append("SECTION ANALYSIS")
lines.append("-" * table_width)
lines.append(f"{'Section':<20} {'Address':<12} {'Size':<12} {'Location'}")
lines.append("-" * table_width)
total_ram_usage = 0
total_flash_usage = 0
for name, section in sorted(self.sections.items(), key=lambda x: x[1].address):
if name in self.ram_sections:
location = "RAM"
total_ram_usage += section.size
elif name in self.flash_sections:
location = "FLASH"
total_flash_usage += section.size
else:
location = "OTHER"
if show_all_sections or name in self.ram_sections:
lines.append(
f"{name:<20} 0x{section.address:08x} {section.size:>8} B {location}"
)
lines.append("-" * table_width)
lines.append(f"Total RAM sections size: {total_ram_usage:,} bytes")
lines.append(f"Total Flash sections size: {total_flash_usage:,} bytes")
# Strings in RAM
lines.append("")
lines.append("=" * table_width)
lines.append("STRINGS IN RAM SECTIONS")
lines.append("=" * table_width)
lines.append(
"Note: .bss sections contain uninitialized data (no strings to extract)"
)
# Group strings by section
strings_by_section: dict[str, list[RamString]] = defaultdict(list)
for ram_string in self.ram_strings:
strings_by_section[ram_string.section].append(ram_string)
for section_name in sorted(strings_by_section.keys()):
section_strings = strings_by_section[section_name]
lines.append(f"\nSection: {section_name}")
lines.append("-" * 40)
for ram_string in sorted(section_strings, key=lambda x: x.address):
clean_string = ram_string.content[:100] + (
"..." if len(ram_string.content) > 100 else ""
)
lines.append(
f' 0x{ram_string.address:08x}: "{clean_string}" (len={len(ram_string.content)})'
)
# Large RAM symbols
lines.append("")
lines.append("=" * table_width)
lines.append("LARGE DATA SYMBOLS IN RAM (>= 50 bytes)")
lines.append("=" * table_width)
largest_symbols = self.get_largest_symbols(50)
lines.append(f"\n{'Symbol':<50} {'Type':<6} {'Size':<10} {'Section'}")
lines.append("-" * table_width)
for symbol in largest_symbols:
# Use demangled name if available, otherwise raw name
display_name = symbol.demangled or symbol.name
name_display = display_name[:49] if len(display_name) > 49 else display_name
lines.append(
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
)
# Summary
lines.append("")
lines.append("=" * table_width)
lines.append("SUMMARY")
lines.append("=" * table_width)
lines.append(f"Total strings found in RAM: {len(self.ram_strings)}")
total_string_bytes = self.get_total_string_bytes()
lines.append(f"Total bytes used by strings: {total_string_bytes:,}")
# Optimization targets
lines.append("")
lines.append("=" * table_width)
lines.append("POTENTIAL OPTIMIZATION TARGETS")
lines.append("=" * table_width)
# Repeated strings
repeated = self.get_repeated_strings()[:10]
if repeated:
lines.append("\nRepeated strings (could be deduplicated):")
for string, count in repeated:
savings = (count - 1) * (len(string) + 1)
clean_string = string[:50] + ("..." if len(string) > 50 else "")
lines.append(
f' "{clean_string}" - appears {count} times (potential savings: {savings} bytes)'
)
# Long strings - platform-specific advice
long_strings = self.get_long_strings(20)[:10]
if long_strings:
if self.platform == "esp8266":
lines.append(
"\nLong strings that could be moved to PROGMEM (>= 20 chars):"
)
else:
# ESP32: strings in DRAM are typically there for a reason
# (interrupt handlers, pre-flash-init code, etc.)
lines.append("\nLong strings in DRAM (>= 20 chars):")
lines.append(
"Note: ESP32 DRAM strings may be required for interrupt/early-boot contexts"
)
for ram_string in long_strings:
clean_string = ram_string.content[:60] + (
"..." if len(ram_string.content) > 60 else ""
)
lines.append(
f' {ram_string.section} @ 0x{ram_string.address:08x}: "{clean_string}" ({len(ram_string.content)} bytes)'
)
lines.append("")
return "\n".join(lines)

View File

@@ -1,93 +0,0 @@
"""Toolchain utilities for memory analysis."""
from __future__ import annotations
import logging
from pathlib import Path
import subprocess
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Sequence
_LOGGER = logging.getLogger(__name__)
# Platform-specific toolchain prefixes
TOOLCHAIN_PREFIXES = [
"xtensa-lx106-elf-", # ESP8266
"xtensa-esp32-elf-", # ESP32
"xtensa-esp-elf-", # ESP32 (newer IDF)
"", # System default (no prefix)
]
def find_tool(
tool_name: str,
objdump_path: str | None = None,
) -> str | None:
"""Find a toolchain tool by name.
First tries to derive the tool path from objdump_path (if provided),
then falls back to searching for platform-specific tools.
Args:
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
objdump_path: Path to objdump binary to derive other tool paths from
Returns:
Path to the tool or None if not found
"""
# Try to derive from objdump path first (most reliable)
if objdump_path and objdump_path != "objdump":
objdump_file = Path(objdump_path)
# Replace just the filename portion, preserving any prefix (e.g., xtensa-esp32-elf-)
new_name = objdump_file.name.replace("objdump", tool_name)
potential_path = str(objdump_file.with_name(new_name))
if Path(potential_path).exists():
_LOGGER.debug("Found %s at: %s", tool_name, potential_path)
return potential_path
# Try platform-specific tools
for prefix in TOOLCHAIN_PREFIXES:
cmd = f"{prefix}{tool_name}"
try:
subprocess.run([cmd, "--version"], capture_output=True, check=True)
_LOGGER.debug("Found %s: %s", tool_name, cmd)
return cmd
except (subprocess.CalledProcessError, FileNotFoundError):
continue
_LOGGER.warning("Could not find %s tool", tool_name)
return None
def run_tool(
cmd: Sequence[str],
timeout: int = 30,
) -> subprocess.CompletedProcess[str] | None:
"""Run a toolchain command and return the result.
Args:
cmd: Command and arguments to run
timeout: Timeout in seconds
Returns:
CompletedProcess on success, None on failure
"""
try:
return subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
check=False,
)
except subprocess.TimeoutExpired:
_LOGGER.warning("Command timed out: %s", " ".join(cmd))
return None
except FileNotFoundError:
_LOGGER.warning("Command not found: %s", cmd[0])
return None
except OSError as e:
_LOGGER.warning("Failed to run command %s: %s", cmd[0], e)
return None

View File

@@ -15,13 +15,8 @@ from esphome.const import (
CONF_TYPE_ID, CONF_TYPE_ID,
CONF_UPDATE_INTERVAL, CONF_UPDATE_INTERVAL,
) )
from esphome.core import ID, Lambda from esphome.core import ID
from esphome.cpp_generator import ( from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
LambdaExpression,
MockObj,
MockObjClass,
TemplateArgsType,
)
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType from esphome.types import ConfigType
from esphome.util import Registry from esphome.util import Registry
@@ -92,7 +87,6 @@ def validate_potentially_or_condition(value):
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
IfAction = cg.esphome_ns.class_("IfAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action)
WhileAction = cg.esphome_ns.class_("WhileAction", Action) WhileAction = cg.esphome_ns.class_("WhileAction", Action)
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
@@ -103,40 +97,9 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
Automation = cg.esphome_ns.class_("Automation") Automation = cg.esphome_ns.class_("Automation")
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component)
def new_lambda_pvariable(
id_obj: ID,
lambda_expr: LambdaExpression,
stateless_class: MockObjClass,
template_arg: cg.TemplateArguments | None = None,
) -> MockObj:
"""Create Pvariable for lambda, using stateless class if applicable.
Combines ID selection and Pvariable creation in one call. For stateless
lambdas (empty capture), uses function pointer instead of std::function.
Args:
id_obj: The ID object (action_id, condition_id, or filter_id)
lambda_expr: The lambda expression object
stateless_class: The stateless class to use for stateless lambdas
template_arg: Optional template arguments (for actions/conditions)
Returns:
The created Pvariable
"""
# For stateless lambdas, use function pointer instead of std::function
if lambda_expr.capture == "":
id_obj = id_obj.copy()
id_obj.type = stateless_class
if template_arg is not None:
return cg.new_Pvariable(id_obj, template_arg, lambda_expr)
return cg.new_Pvariable(id_obj, lambda_expr)
def validate_automation(extra_schema=None, extra_validators=None, single=False): def validate_automation(extra_schema=None, extra_validators=None, single=False):
if extra_schema is None: if extra_schema is None:
extra_schema = {} extra_schema = {}
@@ -182,7 +145,7 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
value = cv.Schema([extra_validators])(value) value = cv.Schema([extra_validators])(value)
if single: if single:
if len(value) != 1: if len(value) != 1:
raise cv.Invalid("This trigger allows only a single automation") raise cv.Invalid("Cannot have more than 1 automation for templates")
return value[0] return value[0]
return value return value
@@ -277,9 +240,7 @@ async def lambda_condition_to_code(
args: TemplateArgsType, args: TemplateArgsType,
) -> MockObj: ) -> MockObj:
lambda_ = await cg.process_lambda(config, args, return_type=bool) lambda_ = await cg.process_lambda(config, args, return_type=bool)
return new_lambda_pvariable( return cg.new_Pvariable(condition_id, template_arg, lambda_)
condition_id, lambda_, StatelessLambdaCondition, template_arg
)
@register_condition( @register_condition(
@@ -310,30 +271,6 @@ async def for_condition_to_code(
return var return var
@register_condition(
"component.is_idle",
LambdaCondition,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.Component),
}
),
)
async def component_is_idle_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
lambda_ = await cg.process_lambda(
Lambda(f"return {comp}->is_idle();"), args, return_type=bool
)
return new_lambda_pvariable(
condition_id, lambda_, StatelessLambdaCondition, template_arg
)
@register_action( @register_action(
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds) "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
) )
@@ -469,7 +406,7 @@ async def lambda_action_to_code(
args: TemplateArgsType, args: TemplateArgsType,
) -> MockObj: ) -> MockObj:
lambda_ = await cg.process_lambda(config, args, return_type=cg.void) lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) return cg.new_Pvariable(action_id, template_arg, lambda_)
@register_action( @register_action(

View File

@@ -62,7 +62,6 @@ from esphome.cpp_types import ( # noqa: F401
EntityBase, EntityBase,
EntityCategory, EntityCategory,
ESPTime, ESPTime,
FixedVector,
GPIOPin, GPIOPin,
InternalGPIOPin, InternalGPIOPin,
JsonObject, JsonObject,

View File

@@ -30,9 +30,7 @@ void A01nyubComponent::check_buffer_() {
ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters); ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters);
this->publish_state(meters); this->publish_state(meters);
} else { } else {
char hex_buf[format_hex_pretty_size(4)]; ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
ESP_LOGW(TAG, "Invalid data read from sensor: %s",
format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_.size()));
} }
} else { } else {
ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);

View File

@@ -29,9 +29,7 @@ void A02yyuwComponent::check_buffer_() {
ESP_LOGV(TAG, "Distance from sensor: %f mm", distance); ESP_LOGV(TAG, "Distance from sensor: %f mm", distance);
this->publish_state(distance); this->publish_state(distance);
} else { } else {
char hex_buf[format_hex_pretty_size(4)]; ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
ESP_LOGW(TAG, "Invalid data read from sensor: %s",
format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_.size()));
} }
} else { } else {
ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);

View File

@@ -87,19 +87,16 @@ void AbsoluteHumidityComponent::loop() {
break; break;
default: default:
this->publish_state(NAN); this->publish_state(NAN);
this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!")); this->status_set_error("Invalid saturation vapor pressure equation selection!");
return; return;
} }
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es);
// Calculate absolute humidity // Calculate absolute humidity
const float absolute_humidity = vapor_density(es, hr, temperature_k); const float absolute_humidity = vapor_density(es, hr, temperature_k);
ESP_LOGD(TAG,
"Saturation vapor pressure %f kPa\n"
"Publishing absolute humidity %f g/m³",
es, absolute_humidity);
// Publish absolute humidity // Publish absolute humidity
ESP_LOGD(TAG, "Publishing absolute humidity %f g/m³", absolute_humidity);
this->status_clear_warning(); this->status_clear_warning();
this->publish_state(absolute_humidity); this->publish_state(absolute_humidity);
} }
@@ -166,7 +163,7 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
} }
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/ // From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
// H/T to https://esphome.io/cookbook/bme280_environment/ // H/T to https://esphome.io/cookbook/bme280_environment.html
// H/T to https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/ // H/T to https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) { float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
// es = saturated vapor pressure (kPa) // es = saturated vapor pressure (kPa)

View File

@@ -1,3 +1,5 @@
#ifdef USE_ARDUINO
#include "ac_dimmer.h" #include "ac_dimmer.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -7,12 +9,12 @@
#ifdef USE_ESP8266 #ifdef USE_ESP8266
#include <core_esp8266_waveform.h> #include <core_esp8266_waveform.h>
#endif #endif
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#ifdef USE_ESP32 #include <esp32-hal-timer.h>
#include "hw_timer_esp_idf.h"
#endif #endif
namespace esphome::ac_dimmer { namespace esphome {
namespace ac_dimmer {
static const char *const TAG = "ac_dimmer"; static const char *const TAG = "ac_dimmer";
@@ -25,14 +27,7 @@ static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-no
/// However other factors like gate driver propagation time /// However other factors like gate driver propagation time
/// are also considered and a really low value is not important /// are also considered and a really low value is not important
/// See also: https://github.com/esphome/issues/issues/1632 /// See also: https://github.com/esphome/issues/issues/1632
static constexpr uint32_t GATE_ENABLE_TIME = 50; static const uint32_t GATE_ENABLE_TIME = 50;
#ifdef USE_ESP32
/// Timer frequency in Hz (1 MHz = 1µs resolution)
static constexpr uint32_t TIMER_FREQUENCY_HZ = 1000000;
/// Timer interrupt interval in microseconds
static constexpr uint64_t TIMER_INTERVAL_US = 50;
#endif
/// Function called from timer interrupt /// Function called from timer interrupt
/// Input is current time in microseconds (micros()) /// Input is current time in microseconds (micros())
@@ -159,7 +154,7 @@ void IRAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) {
#ifdef USE_ESP32 #ifdef USE_ESP32
// ESP32 implementation, uses basically the same code but needs to wrap // ESP32 implementation, uses basically the same code but needs to wrap
// timer_interrupt() function to auto-reschedule // timer_interrupt() function to auto-reschedule
static HWTimer *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static hw_timer_t *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void IRAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); } void IRAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); }
#endif #endif
@@ -199,15 +194,15 @@ void AcDimmer::setup() {
setTimer1Callback(&timer_interrupt); setTimer1Callback(&timer_interrupt);
#endif #endif
#ifdef USE_ESP32 #ifdef USE_ESP32
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ); // timer frequency of 1mhz
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); dimmer_timer = timerBegin(1000000);
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions // For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage). // are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs. // Here we just use an interrupt firing every 50 µs.
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0); timerAlarm(dimmer_timer, 50, true, 0);
#endif #endif
} }
void AcDimmer::write_state(float state) { void AcDimmer::write_state(float state) {
state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
auto new_value = static_cast<uint16_t>(roundf(state * 65535)); auto new_value = static_cast<uint16_t>(roundf(state * 65535));
@@ -215,15 +210,14 @@ void AcDimmer::write_state(float state) {
this->store_.init_cycle = this->init_with_half_cycle_; this->store_.init_cycle = this->init_with_half_cycle_;
this->store_.value = new_value; this->store_.value = new_value;
} }
void AcDimmer::dump_config() { void AcDimmer::dump_config() {
ESP_LOGCONFIG(TAG, "AcDimmer:");
LOG_PIN(" Output Pin: ", this->gate_pin_);
LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_);
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"AcDimmer:\n"
" Min Power: %.1f%%\n" " Min Power: %.1f%%\n"
" Init with half cycle: %s", " Init with half cycle: %s",
this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_)); this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_));
LOG_PIN(" Output Pin: ", this->gate_pin_);
LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_);
if (method_ == DIM_METHOD_LEADING_PULSE) { if (method_ == DIM_METHOD_LEADING_PULSE) {
ESP_LOGCONFIG(TAG, " Method: leading pulse"); ESP_LOGCONFIG(TAG, " Method: leading pulse");
} else if (method_ == DIM_METHOD_LEADING) { } else if (method_ == DIM_METHOD_LEADING) {
@@ -236,4 +230,7 @@ void AcDimmer::dump_config() {
ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2); ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2);
} }
} // namespace esphome::ac_dimmer } // namespace ac_dimmer
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -1,10 +1,13 @@
#pragma once #pragma once
#ifdef USE_ARDUINO
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h" #include "esphome/components/output/float_output.h"
namespace esphome::ac_dimmer { namespace esphome {
namespace ac_dimmer {
enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING }; enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING };
@@ -61,4 +64,7 @@ class AcDimmer : public output::FloatOutput, public Component {
DimMethod method_; DimMethod method_;
}; };
} // namespace esphome::ac_dimmer } // namespace ac_dimmer
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -1,152 +0,0 @@
#ifdef USE_ESP32
#include "hw_timer_esp_idf.h"
#include "freertos/FreeRTOS.h"
#include "esphome/core/log.h"
#include "driver/gptimer.h"
#include "esp_clk_tree.h"
#include "soc/clk_tree_defs.h"
static const char *const TAG = "hw_timer_esp_idf";
namespace esphome::ac_dimmer {
// GPTimer divider constraints from ESP-IDF documentation
static constexpr uint32_t GPTIMER_DIVIDER_MIN = 2;
static constexpr uint32_t GPTIMER_DIVIDER_MAX = 65536;
using voidFuncPtr = void (*)();
using voidFuncPtrArg = void (*)(void *);
struct InterruptConfigT {
voidFuncPtr fn{nullptr};
void *arg{nullptr};
};
struct HWTimer {
gptimer_handle_t timer_handle{nullptr};
InterruptConfigT interrupt_handle{};
bool timer_started{false};
};
HWTimer *timer_begin(uint32_t frequency) {
esp_err_t err = ESP_OK;
uint32_t counter_src_hz = 0;
uint32_t divider = 0;
soc_module_clk_t clk;
for (auto clk_candidate : SOC_GPTIMER_CLKS) {
clk = clk_candidate;
esp_clk_tree_src_get_freq_hz(clk, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &counter_src_hz);
divider = counter_src_hz / frequency;
if ((divider >= GPTIMER_DIVIDER_MIN) && (divider <= GPTIMER_DIVIDER_MAX)) {
break;
} else {
divider = 0;
}
}
if (divider == 0) {
ESP_LOGE(TAG, "Resolution not possible; aborting");
return nullptr;
}
gptimer_config_t config = {
.clk_src = static_cast<gptimer_clock_source_t>(clk),
.direction = GPTIMER_COUNT_UP,
.resolution_hz = frequency,
.flags = {.intr_shared = true},
};
HWTimer *timer = new HWTimer();
err = gptimer_new_timer(&config, &timer->timer_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "GPTimer creation failed; error %d", err);
delete timer;
return nullptr;
}
err = gptimer_enable(timer->timer_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "GPTimer enable failed; error %d", err);
gptimer_del_timer(timer->timer_handle);
delete timer;
return nullptr;
}
err = gptimer_start(timer->timer_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "GPTimer start failed; error %d", err);
gptimer_disable(timer->timer_handle);
gptimer_del_timer(timer->timer_handle);
delete timer;
return nullptr;
}
timer->timer_started = true;
return timer;
}
bool IRAM_ATTR timer_fn_wrapper(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *args) {
auto *isr = static_cast<InterruptConfigT *>(args);
if (isr->fn) {
if (isr->arg) {
reinterpret_cast<voidFuncPtrArg>(isr->fn)(isr->arg);
} else {
isr->fn();
}
}
// Return false to indicate that no higher-priority task was woken and no context switch is requested.
return false;
}
static void timer_attach_interrupt_functional_arg(HWTimer *timer, void (*user_func)(void *), void *arg) {
if (timer == nullptr) {
ESP_LOGE(TAG, "Timer handle is nullptr");
return;
}
gptimer_event_callbacks_t cbs = {
.on_alarm = timer_fn_wrapper,
};
timer->interrupt_handle.fn = reinterpret_cast<voidFuncPtr>(user_func);
timer->interrupt_handle.arg = arg;
if (timer->timer_started) {
gptimer_stop(timer->timer_handle);
}
gptimer_disable(timer->timer_handle);
esp_err_t err = gptimer_register_event_callbacks(timer->timer_handle, &cbs, &timer->interrupt_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Timer Attach Interrupt failed; error %d", err);
}
gptimer_enable(timer->timer_handle);
if (timer->timer_started) {
gptimer_start(timer->timer_handle);
}
}
void timer_attach_interrupt(HWTimer *timer, voidFuncPtr user_func) {
timer_attach_interrupt_functional_arg(timer, reinterpret_cast<voidFuncPtrArg>(user_func), nullptr);
}
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count) {
if (timer == nullptr) {
ESP_LOGE(TAG, "Timer handle is nullptr");
return;
}
gptimer_alarm_config_t alarm_cfg = {
.alarm_count = alarm_value,
.reload_count = reload_count,
.flags = {.auto_reload_on_alarm = autoreload},
};
esp_err_t err = gptimer_set_alarm_action(timer->timer_handle, &alarm_cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Timer Alarm Write failed; error %d", err);
}
}
} // namespace esphome::ac_dimmer
#endif

View File

@@ -1,17 +0,0 @@
#pragma once
#ifdef USE_ESP32
#include "driver/gptimer_types.h"
namespace esphome::ac_dimmer {
struct HWTimer;
HWTimer *timer_begin(uint32_t frequency);
void timer_attach_interrupt(HWTimer *timer, void (*user_func)());
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count);
} // namespace esphome::ac_dimmer
#endif

View File

@@ -3,7 +3,6 @@ import esphome.codegen as cg
from esphome.components import output from esphome.components import output
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_METHOD, CONF_MIN_POWER from esphome.const import CONF_ID, CONF_METHOD, CONF_MIN_POWER
from esphome.core import CORE
CODEOWNERS = ["@glmnet"] CODEOWNERS = ["@glmnet"]
@@ -32,16 +31,11 @@ CONFIG_SCHEMA = cv.All(
), ),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.only_with_arduino,
) )
async def to_code(config): async def to_code(config):
if CORE.is_esp8266:
# ac_dimmer uses setTimer1Callback which requires the waveform generator
from esphome.components.esp8266.const import require_waveform
require_waveform()
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -9,7 +9,7 @@ static const char *const TAG = "adalight_light_effect";
static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; static const uint32_t ADALIGHT_ACK_INTERVAL = 1000;
static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000;
AdalightLightEffect::AdalightLightEffect(const char *name) : AddressableLightEffect(name) {} AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {}
void AdalightLightEffect::start() { void AdalightLightEffect::start() {
AddressableLightEffect::start(); AddressableLightEffect::start();

View File

@@ -11,7 +11,7 @@ namespace adalight {
class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice {
public: public:
AdalightLightEffect(const char *name); AdalightLightEffect(const std::string &name);
void start() override; void start() override;
void stop() override; void stop() override;

View File

@@ -1,17 +1,15 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import ( from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C5, VARIANT_ESP32C5,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
get_esp32_variant,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
@@ -101,13 +99,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
5: adc_channel_t.ADC_CHANNEL_5, 5: adc_channel_t.ADC_CHANNEL_5,
6: adc_channel_t.ADC_CHANNEL_6, 6: adc_channel_t.ADC_CHANNEL_6,
}, },
# https://docs.espressif.com/projects/esp-idf/en/latest/esp32c61/api-reference/peripherals/gpio.html
VARIANT_ESP32C61: {
1: adc_channel_t.ADC_CHANNEL_0,
3: adc_channel_t.ADC_CHANNEL_1,
4: adc_channel_t.ADC_CHANNEL_2,
5: adc_channel_t.ADC_CHANNEL_3,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: { VARIANT_ESP32H2: {
1: adc_channel_t.ADC_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
@@ -116,17 +107,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
4: adc_channel_t.ADC_CHANNEL_3, 4: adc_channel_t.ADC_CHANNEL_3,
5: adc_channel_t.ADC_CHANNEL_4, 5: adc_channel_t.ADC_CHANNEL_4,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h
VARIANT_ESP32P4: {
16: adc_channel_t.ADC_CHANNEL_0,
17: adc_channel_t.ADC_CHANNEL_1,
18: adc_channel_t.ADC_CHANNEL_2,
19: adc_channel_t.ADC_CHANNEL_3,
20: adc_channel_t.ADC_CHANNEL_4,
21: adc_channel_t.ADC_CHANNEL_5,
22: adc_channel_t.ADC_CHANNEL_6,
23: adc_channel_t.ADC_CHANNEL_7,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
1: adc_channel_t.ADC_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
@@ -153,6 +133,16 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
9: adc_channel_t.ADC_CHANNEL_8, 9: adc_channel_t.ADC_CHANNEL_8,
10: adc_channel_t.ADC_CHANNEL_9, 10: adc_channel_t.ADC_CHANNEL_9,
}, },
VARIANT_ESP32P4: {
16: adc_channel_t.ADC_CHANNEL_0,
17: adc_channel_t.ADC_CHANNEL_1,
18: adc_channel_t.ADC_CHANNEL_2,
19: adc_channel_t.ADC_CHANNEL_3,
20: adc_channel_t.ADC_CHANNEL_4,
21: adc_channel_t.ADC_CHANNEL_5,
22: adc_channel_t.ADC_CHANNEL_6,
23: adc_channel_t.ADC_CHANNEL_7,
},
} }
# pin to adc2 channel mapping # pin to adc2 channel mapping
@@ -183,19 +173,8 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
VARIANT_ESP32C5: {}, # no ADC2 VARIANT_ESP32C5: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {}, # no ADC2 VARIANT_ESP32C6: {}, # no ADC2
# ESP32-C61 has no ADC2
VARIANT_ESP32C61: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {}, # no ADC2 VARIANT_ESP32H2: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h
VARIANT_ESP32P4: {
49: adc_channel_t.ADC_CHANNEL_0,
50: adc_channel_t.ADC_CHANNEL_1,
51: adc_channel_t.ADC_CHANNEL_2,
52: adc_channel_t.ADC_CHANNEL_3,
53: adc_channel_t.ADC_CHANNEL_4,
54: adc_channel_t.ADC_CHANNEL_5,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
11: adc_channel_t.ADC_CHANNEL_0, 11: adc_channel_t.ADC_CHANNEL_0,
@@ -222,6 +201,14 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
19: adc_channel_t.ADC_CHANNEL_8, 19: adc_channel_t.ADC_CHANNEL_8,
20: adc_channel_t.ADC_CHANNEL_9, 20: adc_channel_t.ADC_CHANNEL_9,
}, },
VARIANT_ESP32P4: {
49: adc_channel_t.ADC_CHANNEL_0,
50: adc_channel_t.ADC_CHANNEL_1,
51: adc_channel_t.ADC_CHANNEL_2,
52: adc_channel_t.ADC_CHANNEL_3,
53: adc_channel_t.ADC_CHANNEL_4,
54: adc_channel_t.ADC_CHANNEL_5,
},
} }

View File

@@ -42,11 +42,10 @@ void ADCSensor::setup() {
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
init_config.unit_id = this->adc_unit_; init_config.unit_id = this->adc_unit_;
init_config.ulp_mode = ADC_ULP_MODE_DISABLE; init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT;
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 ||
// USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 // USE_ESP32_VARIANT_ESP32H2
esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err);
@@ -75,7 +74,7 @@ void ADCSensor::setup() {
adc_cali_handle_t handle = nullptr; adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
// RISC-V variants and S3 use curve fitting calibration // RISC-V variants and S3 use curve fitting calibration
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
@@ -112,7 +111,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false; this->setup_flags_.calibration_complete = false;
} }
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 #endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
} }
this->setup_flags_.init_complete = true; this->setup_flags_.init_complete = true;
@@ -121,21 +120,23 @@ void ADCSensor::setup() {
void ADCSensor::dump_config() { void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this); LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_); LOG_PIN(" Pin: ", this->pin_);
ESP_LOGCONFIG(TAG,
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s",
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
ESP_LOGCONFIG( ESP_LOGCONFIG(
TAG, TAG,
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s\n"
" Setup Status:\n" " Setup Status:\n"
" Handle Init: %s\n" " Handle Init: %s\n"
" Config: %s\n" " Config: %s\n"
" Calibration: %s\n" " Calibration: %s\n"
" Overall Init: %s", " Overall Init: %s",
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED", this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED",
this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED"); this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED");
@@ -185,11 +186,11 @@ float ADCSensor::sample_fixed_attenuation_() {
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
if (this->calibration_handle_ != nullptr) { if (this->calibration_handle_ != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else // Other ESP32 variants use line fitting calibration #else // Other ESP32 variants use line fitting calibration
adc_cali_delete_scheme_line_fitting(this->calibration_handle_); adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 #endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
this->calibration_handle_ = nullptr; this->calibration_handle_ = nullptr;
} }
} }
@@ -218,7 +219,7 @@ float ADCSensor::sample_autorange_() {
if (this->calibration_handle_ != nullptr) { if (this->calibration_handle_ != nullptr) {
// Delete old calibration handle // Delete old calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else #else
adc_cali_delete_scheme_line_fitting(this->calibration_handle_); adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
@@ -230,7 +231,7 @@ float ADCSensor::sample_autorange_() {
adc_cali_handle_t handle = nullptr; adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_curve_fitting_config_t cali_config = {}; adc_cali_curve_fitting_config_t cali_config = {};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_; cali_config.chan = this->channel_;
@@ -265,7 +266,7 @@ float ADCSensor::sample_autorange_() {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) { if (handle != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_delete_scheme_curve_fitting(handle); adc_cali_delete_scheme_curve_fitting(handle);
#else #else
adc_cali_delete_scheme_line_fitting(handle); adc_cali_delete_scheme_line_fitting(handle);
@@ -287,7 +288,7 @@ float ADCSensor::sample_autorange_() {
} }
// Clean up calibration handle // Clean up calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_delete_scheme_curve_fitting(handle); adc_cali_delete_scheme_curve_fitting(handle);
#else #else
adc_cali_delete_scheme_line_fitting(handle); adc_cali_delete_scheme_line_fitting(handle);

View File

@@ -25,13 +25,11 @@ class AddressableLightDisplay : public display::DisplayBuffer {
if (enabled_ && !enabled) { // enabled -> disabled if (enabled_ && !enabled) { // enabled -> disabled
// - Tell the parent light to refresh, effectively wiping the display. Also // - Tell the parent light to refresh, effectively wiping the display. Also
// restores the previous effect (if any). // restores the previous effect (if any).
if (this->last_effect_index_.has_value()) { light_state_->make_call().set_effect(this->last_effect_).perform();
light_state_->make_call().set_effect(*this->last_effect_index_).perform();
}
} else if (!enabled_ && enabled) { // disabled -> enabled } else if (!enabled_ && enabled) { // disabled -> enabled
// - Save the current effect index. // - Save the current effect.
this->last_effect_index_ = light_state_->get_current_effect_index(); this->last_effect_ = light_state_->get_effect_name();
// - Disable any current effect. // - Disable any current effect.
light_state_->make_call().set_effect(0).perform(); light_state_->make_call().set_effect(0).perform();
} }
@@ -58,7 +56,7 @@ class AddressableLightDisplay : public display::DisplayBuffer {
int32_t width_; int32_t width_;
int32_t height_; int32_t height_;
std::vector<Color> addressable_light_buffer_; std::vector<Color> addressable_light_buffer_;
optional<uint32_t> last_effect_index_; optional<std::string> last_effect_;
optional<std::function<int(int, int)>> pixel_mapper_f_; optional<std::function<int(int, int)>> pixel_mapper_f_;
}; };
} // namespace addressable_light } // namespace addressable_light

View File

@@ -162,13 +162,11 @@ void ADE7880::update() {
} }
void ADE7880::dump_config() { void ADE7880::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG, "ADE7880:");
"ADE7880:\n"
" Frequency: %.0f Hz",
this->frequency_);
LOG_PIN(" IRQ0 Pin: ", this->irq0_pin_); LOG_PIN(" IRQ0 Pin: ", this->irq0_pin_);
LOG_PIN(" IRQ1 Pin: ", this->irq1_pin_); LOG_PIN(" IRQ1 Pin: ", this->irq1_pin_);
LOG_PIN(" RESET Pin: ", this->reset_pin_); LOG_PIN(" RESET Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG, " Frequency: %.0f Hz", this->frequency_);
if (this->channel_a_ != nullptr) { if (this->channel_a_ != nullptr) {
ESP_LOGCONFIG(TAG, " Phase A:"); ESP_LOGCONFIG(TAG, " Phase A:");

View File

@@ -227,7 +227,7 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(ADE7880), cv.GenerateID(): cv.declare_id(ADE7880),
cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All( cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All(
cv.frequency, cv.float_range(min=45.0, max=66.0) cv.frequency, cv.Range(min=45.0, max=66.0)
), ),
cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema,

View File

@@ -24,8 +24,6 @@ from esphome.const import (
UNIT_WATT, UNIT_WATT,
) )
CODEOWNERS = ["@angelnu"]
CONF_CURRENT_A = "current_a" CONF_CURRENT_A = "current_a"
CONF_CURRENT_B = "current_b" CONF_CURRENT_B = "current_b"
CONF_ACTIVE_POWER_A = "active_power_a" CONF_ACTIVE_POWER_A = "active_power_a"

View File

@@ -25,8 +25,7 @@ void ADE7953::setup() {
this->ade_write_8(PGA_V_8, pga_v_); this->ade_write_8(PGA_V_8, pga_v_);
this->ade_write_8(PGA_IA_8, pga_ia_); this->ade_write_8(PGA_IA_8, pga_ia_);
this->ade_write_8(PGA_IB_8, pga_ib_); this->ade_write_8(PGA_IB_8, pga_ib_);
this->ade_write_32(AVGAIN_32, avgain_); this->ade_write_32(AVGAIN_32, vgain_);
this->ade_write_32(BVGAIN_32, bvgain_);
this->ade_write_32(AIGAIN_32, aigain_); this->ade_write_32(AIGAIN_32, aigain_);
this->ade_write_32(BIGAIN_32, bigain_); this->ade_write_32(BIGAIN_32, bigain_);
this->ade_write_32(AWGAIN_32, awgain_); this->ade_write_32(AWGAIN_32, awgain_);
@@ -35,8 +34,7 @@ void ADE7953::setup() {
this->ade_read_8(PGA_V_8, &pga_v_); this->ade_read_8(PGA_V_8, &pga_v_);
this->ade_read_8(PGA_IA_8, &pga_ia_); this->ade_read_8(PGA_IA_8, &pga_ia_);
this->ade_read_8(PGA_IB_8, &pga_ib_); this->ade_read_8(PGA_IB_8, &pga_ib_);
this->ade_read_32(AVGAIN_32, &avgain_); this->ade_read_32(AVGAIN_32, &vgain_);
this->ade_read_32(BVGAIN_32, &bvgain_);
this->ade_read_32(AIGAIN_32, &aigain_); this->ade_read_32(AIGAIN_32, &aigain_);
this->ade_read_32(BIGAIN_32, &bigain_); this->ade_read_32(BIGAIN_32, &bigain_);
this->ade_read_32(AWGAIN_32, &awgain_); this->ade_read_32(AWGAIN_32, &awgain_);
@@ -65,14 +63,13 @@ void ADE7953::dump_config() {
" PGA_V_8: 0x%X\n" " PGA_V_8: 0x%X\n"
" PGA_IA_8: 0x%X\n" " PGA_IA_8: 0x%X\n"
" PGA_IB_8: 0x%X\n" " PGA_IB_8: 0x%X\n"
" AVGAIN_32: 0x%08jX\n" " VGAIN_32: 0x%08jX\n"
" BVGAIN_32: 0x%08jX\n"
" AIGAIN_32: 0x%08jX\n" " AIGAIN_32: 0x%08jX\n"
" BIGAIN_32: 0x%08jX\n" " BIGAIN_32: 0x%08jX\n"
" AWGAIN_32: 0x%08jX\n" " AWGAIN_32: 0x%08jX\n"
" BWGAIN_32: 0x%08jX", " BWGAIN_32: 0x%08jX",
this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) avgain_, (uintmax_t) bvgain_, this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) vgain_, (uintmax_t) aigain_,
(uintmax_t) aigain_, (uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_); (uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_);
} }
#define ADE_PUBLISH_(name, val, factor) \ #define ADE_PUBLISH_(name, val, factor) \

View File

@@ -46,12 +46,7 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
void set_pga_ib(uint8_t pga_ib) { pga_ib_ = pga_ib; } void set_pga_ib(uint8_t pga_ib) { pga_ib_ = pga_ib; }
// Set input gains // Set input gains
void set_vgain(uint32_t vgain) { void set_vgain(uint32_t vgain) { vgain_ = vgain; }
// Datasheet says: "to avoid discrepancies in other registers,
// if AVGAIN is set then BVGAIN should be set to the same value."
avgain_ = vgain;
bvgain_ = vgain;
}
void set_aigain(uint32_t aigain) { aigain_ = aigain; } void set_aigain(uint32_t aigain) { aigain_ = aigain; }
void set_bigain(uint32_t bigain) { bigain_ = bigain; } void set_bigain(uint32_t bigain) { bigain_ = bigain; }
void set_awgain(uint32_t awgain) { awgain_ = awgain; } void set_awgain(uint32_t awgain) { awgain_ = awgain; }
@@ -105,8 +100,7 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
uint8_t pga_v_; uint8_t pga_v_;
uint8_t pga_ia_; uint8_t pga_ia_;
uint8_t pga_ib_; uint8_t pga_ib_;
uint32_t avgain_; uint32_t vgain_;
uint32_t bvgain_;
uint32_t aigain_; uint32_t aigain_;
uint32_t bigain_; uint32_t bigain_;
uint32_t awgain_; uint32_t awgain_;

View File

@@ -21,12 +21,10 @@ void ADS1115Sensor::update() {
void ADS1115Sensor::dump_config() { void ADS1115Sensor::dump_config() {
LOG_SENSOR(" ", "ADS1115 Sensor", this); LOG_SENSOR(" ", "ADS1115 Sensor", this);
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_);
" Multiplexer: %u\n" ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_);
" Gain: %u\n" ESP_LOGCONFIG(TAG, " Resolution: %u", this->resolution_);
" Resolution: %u\n" ESP_LOGCONFIG(TAG, " Sample rate: %u", this->samplerate_);
" Sample rate: %u",
this->multiplexer_, this->gain_, this->resolution_, this->samplerate_);
} }
} // namespace ads1115 } // namespace ads1115

View File

@@ -9,10 +9,8 @@ static const char *const TAG = "ads1118.sensor";
void ADS1118Sensor::dump_config() { void ADS1118Sensor::dump_config() {
LOG_SENSOR(" ", "ADS1118 Sensor", this); LOG_SENSOR(" ", "ADS1118 Sensor", this);
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_);
" Multiplexer: %u\n" ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_);
" Gain: %u",
this->multiplexer_, this->gain_);
} }
float ADS1118Sensor::sample() { float ADS1118Sensor::sample() {

View File

@@ -105,7 +105,7 @@ template<typename... Ts> class AGS10NewI2cAddressAction : public Action<Ts...>,
public: public:
TEMPLATABLE_VALUE(uint8_t, new_address) TEMPLATABLE_VALUE(uint8_t, new_address)
void play(const Ts &...x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); }
}; };
enum AGS10SetZeroPointActionMode { enum AGS10SetZeroPointActionMode {
@@ -122,7 +122,7 @@ template<typename... Ts> class AGS10SetZeroPointAction : public Action<Ts...>, p
TEMPLATABLE_VALUE(uint16_t, value) TEMPLATABLE_VALUE(uint16_t, value)
TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode) TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode)
void play(const Ts &...x) override { void play(Ts... x) override {
switch (this->mode_.value(x...)) { switch (this->mode_.value(x...)) {
case FACTORY_DEFAULT: case FACTORY_DEFAULT:
this->parent_->set_zero_point_with_factory_defaults(); this->parent_->set_zero_point_with_factory_defaults();

View File

@@ -83,7 +83,7 @@ void AHT10Component::setup() {
void AHT10Component::restart_read_() { void AHT10Component::restart_read_() {
if (this->read_count_ == AHT10_ATTEMPTS) { if (this->read_count_ == AHT10_ATTEMPTS) {
this->read_count_ = 0; this->read_count_ = 0;
this->status_set_error(LOG_STR("Reading timed out")); this->status_set_error("Reading timed out");
return; return;
} }
this->read_count_++; this->read_count_++;

View File

@@ -13,7 +13,7 @@ template<typename... Ts> class SetAutoMuteAction : public Action<Ts...> {
TEMPLATABLE_VALUE(uint8_t, auto_mute_mode) TEMPLATABLE_VALUE(uint8_t, auto_mute_mode)
void play(const Ts &...x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } void play(Ts... x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); }
protected: protected:
AIC3204 *aic3204_; AIC3204 *aic3204_;

View File

@@ -20,8 +20,7 @@ bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &devic
sn |= ((uint32_t) it.data[2] << 16); sn |= ((uint32_t) it.data[2] << 16);
sn |= ((uint32_t) it.data[3] << 24); sn |= ((uint32_t) it.data[3] << 24);
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; ESP_LOGD(TAG, "Found AirThings device Serial:%" PRIu32 " (MAC: %s)", sn, device.address_str().c_str());
ESP_LOGD(TAG, "Found AirThings device Serial:%" PRIu32 " (MAC: %s)", sn, device.address_str_to(addr_buf));
return true; return true;
} }
} }

View File

@@ -1,5 +1,4 @@
#include "airthings_wave_base.h" #include "airthings_wave_base.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
// All information related to reading battery information came from the sensors.airthings_wave // All information related to reading battery information came from the sensors.airthings_wave
// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave) // project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave)
@@ -94,10 +93,8 @@ void AirthingsWaveBase::update() {
bool AirthingsWaveBase::request_read_values_() { bool AirthingsWaveBase::request_read_values_() {
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_); auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_);
if (chr == nullptr) { if (chr == nullptr) {
char service_buf[esp32_ble::UUID_STR_LEN]; ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
char char_buf[esp32_ble::UUID_STR_LEN]; this->sensors_data_characteristic_uuid_.to_string().c_str());
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_str(service_buf),
this->sensors_data_characteristic_uuid_.to_str(char_buf));
return false; return false;
} }
@@ -120,20 +117,17 @@ bool AirthingsWaveBase::request_battery_() {
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->access_control_point_characteristic_uuid_); auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->access_control_point_characteristic_uuid_);
if (chr == nullptr) { if (chr == nullptr) {
char service_buf[esp32_ble::UUID_STR_LEN];
char char_buf[esp32_ble::UUID_STR_LEN];
ESP_LOGW(TAG, "No access control point characteristic found at service %s char %s", ESP_LOGW(TAG, "No access control point characteristic found at service %s char %s",
this->service_uuid_.to_str(service_buf), this->access_control_point_characteristic_uuid_.to_str(char_buf)); this->service_uuid_.to_string().c_str(),
this->access_control_point_characteristic_uuid_.to_string().c_str());
return false; return false;
} }
auto *descr = this->parent()->get_descriptor(this->service_uuid_, this->access_control_point_characteristic_uuid_, auto *descr = this->parent()->get_descriptor(this->service_uuid_, this->access_control_point_characteristic_uuid_,
CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID); CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID);
if (descr == nullptr) { if (descr == nullptr) {
char service_buf[esp32_ble::UUID_STR_LEN]; ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_string().c_str(),
char char_buf[esp32_ble::UUID_STR_LEN]; this->access_control_point_characteristic_uuid_.to_string().c_str());
ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_str(service_buf),
this->access_control_point_characteristic_uuid_.to_str(char_buf));
return false; return false;
} }

View File

@@ -172,6 +172,12 @@ def alarm_control_panel_schema(
return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema) return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
# Remove before 2025.11.0
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
cv.deprecated_schema_constant("alarm_control_panel")
)
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
{ {
cv.GenerateID(): cv.use_id(AlarmControlPanel), cv.GenerateID(): cv.use_id(AlarmControlPanel),

View File

@@ -1,14 +1,13 @@
#include "alarm_control_panel.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include <utility> #include <utility>
#include "alarm_control_panel.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::alarm_control_panel { namespace esphome {
namespace alarm_control_panel {
static const char *const TAG = "alarm_control_panel"; static const char *const TAG = "alarm_control_panel";
@@ -34,12 +33,23 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)), ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state; this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY) if (state == ACP_STATE_TRIGGERED) {
ControllerRegistry::notify_alarm_control_panel_update(this); this->triggered_callback_.call();
#endif } else if (state == ACP_STATE_ARMING) {
// Cleared fires when leaving TRIGGERED state this->arming_callback_.call();
} else if (state == ACP_STATE_PENDING) {
this->pending_callback_.call();
} else if (state == ACP_STATE_ARMED_HOME) {
this->armed_home_callback_.call();
} else if (state == ACP_STATE_ARMED_NIGHT) {
this->armed_night_callback_.call();
} else if (state == ACP_STATE_ARMED_AWAY) {
this->armed_away_callback_.call();
} else if (state == ACP_STATE_DISARMED) {
this->disarmed_callback_.call();
}
if (prev_state == ACP_STATE_TRIGGERED) { if (prev_state == ACP_STATE_TRIGGERED) {
this->cleared_callback_.call(); this->cleared_callback_.call();
} }
@@ -54,6 +64,34 @@ void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback)
this->state_callback_.add(std::move(callback)); this->state_callback_.add(std::move(callback));
} }
void AlarmControlPanel::add_on_triggered_callback(std::function<void()> &&callback) {
this->triggered_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_arming_callback(std::function<void()> &&callback) {
this->arming_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_home_callback(std::function<void()> &&callback) {
this->armed_home_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_night_callback(std::function<void()> &&callback) {
this->armed_night_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_away_callback(std::function<void()> &&callback) {
this->armed_away_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_pending_callback(std::function<void()> &&callback) {
this->pending_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_disarmed_callback(std::function<void()> &&callback) {
this->disarmed_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) { void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
this->cleared_callback_.add(std::move(callback)); this->cleared_callback_.add(std::move(callback));
} }
@@ -114,4 +152,5 @@ void AlarmControlPanel::disarm(optional<std::string> code) {
call.perform(); call.perform();
} }
} // namespace esphome::alarm_control_panel } // namespace alarm_control_panel
} // namespace esphome

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <map>
#include "alarm_control_panel_call.h" #include "alarm_control_panel_call.h"
#include "alarm_control_panel_state.h" #include "alarm_control_panel_state.h"
@@ -7,7 +9,8 @@
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::alarm_control_panel { namespace esphome {
namespace alarm_control_panel {
enum AlarmControlPanelFeature : uint8_t { enum AlarmControlPanelFeature : uint8_t {
// Matches Home Assistant values // Matches Home Assistant values
@@ -32,13 +35,54 @@ class AlarmControlPanel : public EntityBase {
*/ */
void publish_state(AlarmControlPanelState state); void publish_state(AlarmControlPanelState state);
/** Add a callback for when the state of the alarm_control_panel changes. /** Add a callback for when the state of the alarm_control_panel changes
* Triggers can check get_state() to determine the new state.
* *
* @param callback The callback function * @param callback The callback function
*/ */
void add_on_state_callback(std::function<void()> &&callback); void add_on_state_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to triggered
*
* @param callback The callback function
*/
void add_on_triggered_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to arming
*
* @param callback The callback function
*/
void add_on_arming_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to pending
*
* @param callback The callback function
*/
void add_on_pending_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_home
*
* @param callback The callback function
*/
void add_on_armed_home_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_night
*
* @param callback The callback function
*/
void add_on_armed_night_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_away
*
* @param callback The callback function
*/
void add_on_armed_away_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to disarmed
*
* @param callback The callback function
*/
void add_on_disarmed_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel clears from triggered /** Add a callback for when the state of the alarm_control_panel clears from triggered
* *
* @param callback The callback function * @param callback The callback function
@@ -128,14 +172,29 @@ class AlarmControlPanel : public EntityBase {
uint32_t last_update_; uint32_t last_update_;
// the call control function // the call control function
virtual void control(const AlarmControlPanelCall &call) = 0; virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback - triggers check get_state() for specific state // state callback
LazyCallbackManager<void()> state_callback_{}; CallbackManager<void()> state_callback_{};
// clear callback - fires when leaving TRIGGERED state // trigger callback
LazyCallbackManager<void()> cleared_callback_{}; CallbackManager<void()> triggered_callback_{};
// arming callback
CallbackManager<void()> arming_callback_{};
// pending callback
CallbackManager<void()> pending_callback_{};
// armed_home callback
CallbackManager<void()> armed_home_callback_{};
// armed_night callback
CallbackManager<void()> armed_night_callback_{};
// armed_away callback
CallbackManager<void()> armed_away_callback_{};
// disarmed callback
CallbackManager<void()> disarmed_callback_{};
// clear callback
CallbackManager<void()> cleared_callback_{};
// chime callback // chime callback
LazyCallbackManager<void()> chime_callback_{}; CallbackManager<void()> chime_callback_{};
// ready callback // ready callback
LazyCallbackManager<void()> ready_callback_{}; CallbackManager<void()> ready_callback_{};
}; };
} // namespace esphome::alarm_control_panel } // namespace alarm_control_panel
} // namespace esphome

View File

@@ -4,7 +4,8 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::alarm_control_panel { namespace esphome {
namespace alarm_control_panel {
static const char *const TAG = "alarm_control_panel"; static const char *const TAG = "alarm_control_panel";
@@ -98,4 +99,5 @@ void AlarmControlPanelCall::perform() {
} }
} }
} // namespace esphome::alarm_control_panel } // namespace alarm_control_panel
} // namespace esphome

View File

@@ -6,7 +6,8 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
namespace esphome::alarm_control_panel { namespace esphome {
namespace alarm_control_panel {
class AlarmControlPanel; class AlarmControlPanel;
@@ -35,4 +36,5 @@ class AlarmControlPanelCall {
void validate_(); void validate_();
}; };
} // namespace esphome::alarm_control_panel } // namespace alarm_control_panel
} // namespace esphome

View File

@@ -1,6 +1,7 @@
#include "alarm_control_panel_state.h" #include "alarm_control_panel_state.h"
namespace esphome::alarm_control_panel { namespace esphome {
namespace alarm_control_panel {
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) {
switch (state) { switch (state) {
@@ -29,4 +30,5 @@ const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState stat
} }
} }
} // namespace esphome::alarm_control_panel } // namespace alarm_control_panel
} // namespace esphome

View File

@@ -3,7 +3,8 @@
#include <cstdint> #include <cstdint>
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::alarm_control_panel { namespace esphome {
namespace alarm_control_panel {
enum AlarmControlPanelState : uint8_t { enum AlarmControlPanelState : uint8_t {
ACP_STATE_DISARMED = 0, ACP_STATE_DISARMED = 0,
@@ -24,4 +25,5 @@ enum AlarmControlPanelState : uint8_t {
*/ */
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state); const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state);
} // namespace esphome::alarm_control_panel } // namespace alarm_control_panel
} // namespace esphome

View File

@@ -3,9 +3,9 @@
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "alarm_control_panel.h" #include "alarm_control_panel.h"
namespace esphome::alarm_control_panel { namespace esphome {
namespace alarm_control_panel {
/// Trigger on any state change
class StateTrigger : public Trigger<> { class StateTrigger : public Trigger<> {
public: public:
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -13,30 +13,55 @@ class StateTrigger : public Trigger<> {
} }
}; };
/// Template trigger that fires when entering a specific state class TriggeredTrigger : public Trigger<> {
template<AlarmControlPanelState State> class StateEnterTrigger : public Trigger<> {
public: public:
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_state_callback([this]() { alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); });
if (this->alarm_control_panel_->get_state() == State)
this->trigger();
});
} }
protected:
AlarmControlPanel *alarm_control_panel_;
}; };
// Type aliases for state-specific triggers class ArmingTrigger : public Trigger<> {
using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>; public:
using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>; explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) {
using PendingTrigger = StateEnterTrigger<ACP_STATE_PENDING>; alarm_control_panel->add_on_arming_callback([this]() { this->trigger(); });
using ArmedHomeTrigger = StateEnterTrigger<ACP_STATE_ARMED_HOME>; }
using ArmedNightTrigger = StateEnterTrigger<ACP_STATE_ARMED_NIGHT>; };
using ArmedAwayTrigger = StateEnterTrigger<ACP_STATE_ARMED_AWAY>;
using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>; class PendingTrigger : public Trigger<> {
public:
explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_pending_callback([this]() { this->trigger(); });
}
};
class ArmedHomeTrigger : public Trigger<> {
public:
explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_home_callback([this]() { this->trigger(); });
}
};
class ArmedNightTrigger : public Trigger<> {
public:
explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_night_callback([this]() { this->trigger(); });
}
};
class ArmedAwayTrigger : public Trigger<> {
public:
explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_away_callback([this]() { this->trigger(); });
}
};
class DisarmedTrigger : public Trigger<> {
public:
explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_disarmed_callback([this]() { this->trigger(); });
}
};
/// Trigger when leaving TRIGGERED state (alarm cleared)
class ClearedTrigger : public Trigger<> { class ClearedTrigger : public Trigger<> {
public: public:
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -44,7 +69,6 @@ class ClearedTrigger : public Trigger<> {
} }
}; };
/// Trigger on chime event (zone opened while disarmed)
class ChimeTrigger : public Trigger<> { class ChimeTrigger : public Trigger<> {
public: public:
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -52,7 +76,6 @@ class ChimeTrigger : public Trigger<> {
} }
}; };
/// Trigger on ready state change
class ReadyTrigger : public Trigger<> { class ReadyTrigger : public Trigger<> {
public: public:
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -66,7 +89,7 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...); auto code = this->code_.optional_value(x...);
if (code.has_value()) { if (code.has_value()) {
@@ -86,7 +109,7 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...); auto code = this->code_.optional_value(x...);
if (code.has_value()) { if (code.has_value()) {
@@ -106,7 +129,7 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...); auto code = this->code_.optional_value(x...);
if (code.has_value()) { if (code.has_value()) {
@@ -126,7 +149,7 @@ template<typename... Ts> class DisarmAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } void play(Ts... x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); }
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -136,7 +159,7 @@ template<typename... Ts> class PendingAction : public Action<Ts...> {
public: public:
explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
void play(const Ts &...x) override { this->alarm_control_panel_->make_call().pending().perform(); } void play(Ts... x) override { this->alarm_control_panel_->make_call().pending().perform(); }
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -146,7 +169,7 @@ template<typename... Ts> class TriggeredAction : public Action<Ts...> {
public: public:
explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
void play(const Ts &...x) override { this->alarm_control_panel_->make_call().triggered().perform(); } void play(Ts... x) override { this->alarm_control_panel_->make_call().triggered().perform(); }
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -155,7 +178,7 @@ template<typename... Ts> class TriggeredAction : public Action<Ts...> {
template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts...> { template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts...> {
public: public:
AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {} AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {}
bool check(const Ts &...x) override { bool check(Ts... x) override {
return this->parent_->is_state_armed(this->parent_->get_state()) || return this->parent_->is_state_armed(this->parent_->get_state()) ||
this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED; this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED;
} }
@@ -164,4 +187,5 @@ template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts.
AlarmControlPanel *parent_; AlarmControlPanel *parent_;
}; };
} // namespace esphome::alarm_control_panel } // namespace alarm_control_panel
} // namespace esphome

View File

@@ -56,13 +56,13 @@ bool Alpha3::is_current_response_type_(const uint8_t *response_type) {
void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
if (this->response_offset_ >= this->response_length_) { if (this->response_offset_ >= this->response_length_) {
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str()); ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str());
if (length < GENI_RESPONSE_HEADER_LENGTH) { if (length < GENI_RESPONSE_HEADER_LENGTH) {
ESP_LOGW(TAG, "[%s] response too short", this->parent_->address_str()); ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str());
return; return;
} }
if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) { if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) {
ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str(), ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(),
response[0], response[1], response[2], response[3], response[4]); response[0], response[1], response[2], response[3], response[4]);
return; return;
} }
@@ -77,11 +77,11 @@ void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
}; };
if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) { if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) {
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str()); ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str());
extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F); extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F);
extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F); extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F);
} else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) { } else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) {
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str()); ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str());
extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F); extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F);
extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F); extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F);
extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F); extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F);
@@ -100,7 +100,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
if (param->open.status == ESP_GATT_OK) { if (param->open.status == ESP_GATT_OK) {
this->response_offset_ = 0; this->response_offset_ = 0;
this->response_length_ = 0; this->response_length_ = 0;
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str()); ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str());
} }
break; break;
} }
@@ -132,7 +132,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
case ESP_GATTC_SEARCH_CMPL_EVT: { case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID); auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID);
if (chr == nullptr) { if (chr == nullptr) {
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str()); ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str());
break; break;
} }
auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
@@ -164,12 +164,12 @@ void Alpha3::send_request_(uint8_t *request, size_t len) {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len, esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) if (status)
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} }
void Alpha3::update() { void Alpha3::update() {
if (this->node_state != espbt::ClientState::ESTABLISHED) { if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str()); ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
return; return;
} }

View File

@@ -15,8 +15,10 @@ namespace alpha3 {
namespace espbt = esphome::esp32_ble_tracker; namespace espbt = esphome::esp32_ble_tracker;
static const espbt::ESPBTUUID ALPHA3_GENI_SERVICE_UUID = espbt::ESPBTUUID::from_uint16(0xfe5d); static const espbt::ESPBTUUID ALPHA3_GENI_SERVICE_UUID = espbt::ESPBTUUID::from_uint16(0xfe5d);
static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID = espbt::ESPBTUUID::from_raw( static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID =
{0xa9, 0x7b, 0xb8, 0x85, 0x00, 0x1a, 0x28, 0xaa, 0x2a, 0x43, 0x6e, 0x03, 0xd1, 0xff, 0x9c, 0x85}); espbt::ESPBTUUID::from_raw({static_cast<char>(0xa9), 0x7b, static_cast<char>(0xb8), static_cast<char>(0x85), 0x0,
0x1a, 0x28, static_cast<char>(0xaa), 0x2a, 0x43, 0x6e, 0x3, static_cast<char>(0xd1),
static_cast<char>(0xff), static_cast<char>(0x9c), static_cast<char>(0x85)});
static const int16_t GENI_RESPONSE_HEADER_LENGTH = 13; static const int16_t GENI_RESPONSE_HEADER_LENGTH = 13;
static const size_t GENI_RESPONSE_TYPE_LENGTH = 8; static const size_t GENI_RESPONSE_TYPE_LENGTH = 8;

View File

@@ -44,9 +44,11 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID);
if (chr == nullptr) { if (chr == nullptr) {
if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) {
ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->parent_->address_str()); ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.",
this->parent_->address_str().c_str());
} else { } else {
ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->parent_->address_str()); ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?",
this->parent_->address_str().c_str());
} }
break; break;
} }
@@ -80,7 +82,8 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
this->char_handle_, packet->length, packet->data, this->char_handle_, packet->length, packet->data,
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) { if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
status);
} }
} }
this->current_sensor_ = 0; this->current_sensor_ = 0;
@@ -94,7 +97,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
void Am43::update() { void Am43::update() {
if (this->node_state != espbt::ClientState::ESTABLISHED) { if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str()); ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
return; return;
} }
if (this->current_sensor_ == 0) { if (this->current_sensor_ == 0) {
@@ -104,7 +107,7 @@ void Am43::update() {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) { if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} }
} }
this->current_sensor_++; this->current_sensor_++;

View File

@@ -12,11 +12,10 @@ void AnalogThresholdBinarySensor::setup() {
// TRUE state is defined to be when sensor is >= threshold // TRUE state is defined to be when sensor is >= threshold
// so when undefined sensor value initialize to FALSE // so when undefined sensor value initialize to FALSE
if (std::isnan(sensor_value)) { if (std::isnan(sensor_value)) {
this->raw_state_ = false;
this->publish_initial_state(false); this->publish_initial_state(false);
} else { } else {
this->raw_state_ = sensor_value >= (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f; this->publish_initial_state(sensor_value >=
this->publish_initial_state(this->raw_state_); (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f);
} }
} }
@@ -26,10 +25,8 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
this->sensor_->add_on_state_callback([this](float sensor_value) { this->sensor_->add_on_state_callback([this](float sensor_value) {
// if there is an invalid sensor reading, ignore the change and keep the current state // if there is an invalid sensor reading, ignore the change and keep the current state
if (!std::isnan(sensor_value)) { if (!std::isnan(sensor_value)) {
// Use raw_state_ for hysteresis logic, not this->state which is post-filter this->publish_state(sensor_value >=
this->raw_state_ = (this->state ? this->lower_threshold_.value() : this->upper_threshold_.value()));
sensor_value >= (this->raw_state_ ? this->lower_threshold_.value() : this->upper_threshold_.value());
this->publish_state(this->raw_state_);
} }
}); });
} }

View File

@@ -20,7 +20,6 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
sensor::Sensor *sensor_{nullptr}; sensor::Sensor *sensor_{nullptr};
TemplatableValue<float> upper_threshold_{}; TemplatableValue<float> upper_threshold_{};
TemplatableValue<float> lower_threshold_{}; TemplatableValue<float> lower_threshold_{};
bool raw_state_{false}; // Pre-filter state for hysteresis logic
}; };
} // namespace analog_threshold } // namespace analog_threshold

View File

@@ -39,7 +39,7 @@ class Animation : public image::Image {
template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> { template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
public: public:
AnimationNextFrameAction(Animation *parent) : parent_(parent) {} AnimationNextFrameAction(Animation *parent) : parent_(parent) {}
void play(const Ts &...x) override { this->parent_->next_frame(); } void play(Ts... x) override { this->parent_->next_frame(); }
protected: protected:
Animation *parent_; Animation *parent_;
@@ -48,7 +48,7 @@ template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> { template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> {
public: public:
AnimationPrevFrameAction(Animation *parent) : parent_(parent) {} AnimationPrevFrameAction(Animation *parent) : parent_(parent) {}
void play(const Ts &...x) override { this->parent_->prev_frame(); } void play(Ts... x) override { this->parent_->prev_frame(); }
protected: protected:
Animation *parent_; Animation *parent_;
@@ -58,7 +58,7 @@ template<typename... Ts> class AnimationSetFrameAction : public Action<Ts...> {
public: public:
AnimationSetFrameAction(Animation *parent) : parent_(parent) {} AnimationSetFrameAction(Animation *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(uint16_t, frame) TEMPLATABLE_VALUE(uint16_t, frame)
void play(const Ts &...x) override { this->parent_->set_frame(this->frame_.value(x...)); } void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); }
protected: protected:
Animation *parent_; Animation *parent_;

View File

@@ -42,7 +42,7 @@ void Anova::control(const ClimateCall &call) {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) { if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} }
} }
if (call.get_target_temperature().has_value()) { if (call.get_target_temperature().has_value()) {
@@ -51,7 +51,7 @@ void Anova::control(const ClimateCall &call) {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) { if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} }
} }
} }
@@ -67,10 +67,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
case ESP_GATTC_SEARCH_CMPL_EVT: { case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID);
if (chr == nullptr) { if (chr == nullptr) {
ESP_LOGW(TAG, ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str());
"[%s] No control service found at device, not an Anova..?\n" ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str());
"[%s] Note, this component does not currently support Anova Nano.",
this->get_name().c_str(), this->get_name().c_str());
break; break;
} }
this->char_handle_ = chr->handle; this->char_handle_ = chr->handle;
@@ -126,7 +124,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) { if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
status);
} }
} }
} }
@@ -151,7 +150,7 @@ void Anova::update() {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) { if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} }
this->current_request_++; this->current_request_++;
} }

View File

@@ -28,7 +28,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
void dump_config() override; void dump_config() override;
climate::ClimateTraits traits() override { climate::ClimateTraits traits() override {
auto traits = climate::ClimateTraits(); auto traits = climate::ClimateTraits();
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); traits.set_supports_current_temperature(true);
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT}); traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT});
traits.set_visual_min_temperature(25.0); traits.set_visual_min_temperature(25.0);
traits.set_visual_max_temperature(100.0); traits.set_visual_max_temperature(100.0);

View File

@@ -27,13 +27,12 @@ from esphome.const import (
CONF_SERVICE, CONF_SERVICE,
CONF_SERVICES, CONF_SERVICES,
CONF_TAG, CONF_TAG,
CONF_THEN,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_VARIABLES, CONF_VARIABLES,
) )
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.cpp_generator import TemplateArgsType
from esphome.types import ConfigFragmentType, ConfigType from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -64,24 +63,18 @@ HomeAssistantActionResponseTrigger = api_ns.class_(
"HomeAssistantActionResponseTrigger", automation.Trigger "HomeAssistantActionResponseTrigger", automation.Trigger
) )
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition) APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
APIRespondAction = api_ns.class_("APIRespondAction", automation.Action)
APIUnregisterServiceCallAction = api_ns.class_(
"APIUnregisterServiceCallAction", automation.Action
)
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger) UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument") ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument")
SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = { SERVICE_ARG_NATIVE_TYPES = {
"bool": cg.bool_, "bool": bool,
"int": cg.int32, "int": cg.int32,
"float": cg.float_, "float": float,
"string": cg.std_string, "string": cg.std_string,
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"), "bool[]": cg.std_vector.template(bool),
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), "int[]": cg.std_vector.template(cg.int32),
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"), "float[]": cg.std_vector.template(float),
"string[]": cg.FixedVector.template(cg.std_string) "string[]": cg.std_vector.template(cg.std_string),
.operator("const")
.operator("ref"),
} }
CONF_ENCRYPTION = "encryption" CONF_ENCRYPTION = "encryption"
CONF_BATCH_DELAY = "batch_delay" CONF_BATCH_DELAY = "batch_delay"
@@ -90,7 +83,6 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states" CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog" CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue" CONF_MAX_SEND_QUEUE = "max_send_queue"
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
def validate_encryption_key(value): def validate_encryption_key(value):
@@ -107,85 +99,6 @@ def validate_encryption_key(value):
return value return value
CONF_SUPPORTS_RESPONSE = "supports_response"
# Enum values in api::enums namespace
enums_ns = api_ns.namespace("enums")
SUPPORTS_RESPONSE_OPTIONS = {
"none": enums_ns.SUPPORTS_RESPONSE_NONE,
"optional": enums_ns.SUPPORTS_RESPONSE_OPTIONAL,
"only": enums_ns.SUPPORTS_RESPONSE_ONLY,
"status": enums_ns.SUPPORTS_RESPONSE_STATUS,
}
def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
"""Auto-detect supports_response based on api.respond usage in the action's then block.
- If api.respond with data found: set to "optional" (unless user explicitly set)
- If api.respond without data found: set to "status" (unless user explicitly set)
- If no api.respond found: set to "none" (unless user explicitly set)
"""
def scan_actions(items: ConfigFragmentType) -> tuple[bool, bool]:
"""Recursively scan actions for api.respond.
Returns: (found, has_data) tuple - has_data is True if ANY api.respond has data
"""
found_any = False
has_data_any = False
if isinstance(items, list):
for item in items:
found, has_data = scan_actions(item)
if found:
found_any = True
has_data_any = has_data_any or has_data
elif isinstance(items, dict):
# Check if this is an api.respond action
if "api.respond" in items:
respond_config = items["api.respond"]
has_data = isinstance(respond_config, dict) and "data" in respond_config
return True, has_data
# Recursively check all values
for value in items.values():
found, has_data = scan_actions(value)
if found:
found_any = True
has_data_any = has_data_any or has_data
return found_any, has_data_any
then = config.get(CONF_THEN, [])
action_name = config.get(CONF_ACTION)
found, has_data = scan_actions(then)
# If user explicitly set supports_response, validate and use that
if CONF_SUPPORTS_RESPONSE in config:
user_value = config[CONF_SUPPORTS_RESPONSE]
# Validate: "only" requires api.respond with data
if user_value == "only" and not has_data:
raise cv.Invalid(
f"Action '{action_name}' has supports_response=only but no api.respond "
"action with 'data:' was found. Use 'status' for responses without data, "
"or add 'data:' to your api.respond action."
)
return config
# Auto-detect based on api.respond usage
if found:
config[CONF_SUPPORTS_RESPONSE] = "optional" if has_data else "status"
else:
config[CONF_SUPPORTS_RESPONSE] = "none"
return config
def _validate_supports_response(value):
"""Validate supports_response after auto-detection has set the value."""
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
ACTIONS_SCHEMA = automation.validate_automation( ACTIONS_SCHEMA = automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
@@ -196,20 +109,10 @@ ACTIONS_SCHEMA = automation.validate_automation(
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True), cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
} }
), ),
# No default - auto-detected by _auto_detect_supports_response
cv.Optional(CONF_SUPPORTS_RESPONSE): cv.enum(
SUPPORTS_RESPONSE_OPTIONS, lower=True
),
}, },
cv.All( cv.All(
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
cv.rename_key(CONF_SERVICE, CONF_ACTION), cv.rename_key(CONF_SERVICE, CONF_ACTION),
_auto_detect_supports_response,
# Re-validate supports_response after auto-detection sets it
cv.Schema(
{cv.Required(CONF_SUPPORTS_RESPONSE): _validate_supports_response},
extra=cv.ALLOW_EXTRA,
),
), ),
) )
@@ -226,14 +129,29 @@ def _encryption_schema(config):
return ENCRYPTION_SCHEMA(config) return ENCRYPTION_SCHEMA(config)
def _consume_api_sockets(config: ConfigType) -> ConfigType: def _validate_api_config(config: ConfigType) -> ConfigType:
"""Register socket needs for API component.""" """Validate API configuration with mutual exclusivity check and deprecation warning."""
from esphome.components import socket # Check if both password and encryption are configured
has_password = CONF_PASSWORD in config and config[CONF_PASSWORD]
has_encryption = CONF_ENCRYPTION in config
if has_password and has_encryption:
raise cv.Invalid(
"The 'password' and 'encryption' options are mutually exclusive. "
"The API client only supports one authentication method at a time. "
"Please remove one of them. "
"Note: 'password' authentication is deprecated and will be removed in version 2026.1.0. "
"We strongly recommend using 'encryption' instead for better security."
)
# Warn about password deprecation
if has_password:
_LOGGER.warning(
"API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. "
"Please migrate to the 'encryption' configuration. "
"See https://esphome.io/components/api.html#configuration-variables"
)
# API needs 1 listening socket + typically 3 concurrent client connections
# (not max_connections, which is the upper limit rarely reached)
sockets_needed = 1 + 3
socket.consume_sockets(sockets_needed, "api")(config)
return config return config
@@ -242,17 +160,7 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(APIServer), cv.GenerateID(): cv.declare_id(APIServer),
cv.Optional(CONF_PORT, default=6053): cv.port, cv.Optional(CONF_PORT, default=6053): cv.port,
# Removed in 2026.1.0 - kept to provide helpful error message cv.Optional(CONF_PASSWORD, default=""): cv.string_strict,
cv.Optional(CONF_PASSWORD): cv.invalid(
"The 'password' option has been removed in ESPHome 2026.1.0.\n"
"Password authentication was deprecated in May 2022.\n"
"Please migrate to encryption for secure API communication:\n\n"
"api:\n"
" encryption:\n"
" key: !secret api_encryption_key\n\n"
"Generate a key with: openssl rand -base64 32\n"
"Or visit https://esphome.io/components/api/#configuration-variables"
),
cv.Optional( cv.Optional(
CONF_REBOOT_TIMEOUT, default="15min" CONF_REBOOT_TIMEOUT, default="15min"
): cv.positive_time_period_milliseconds, ): cv.positive_time_period_milliseconds,
@@ -306,7 +214,6 @@ CONFIG_SCHEMA = cv.All(
esp32=8, # More RAM, can buffer more esp32=8, # More RAM, can buffer more
rp2040=5, # Limited RAM rp2040=5, # Limited RAM
bk72xx=8, # Moderate RAM bk72xx=8, # Moderate RAM
nrf52=8, # Moderate RAM
rtl87xx=8, # Moderate RAM rtl87xx=8, # Moderate RAM
host=16, # Abundant resources host=16, # Abundant resources
ln882x=8, # Moderate RAM ln882x=8, # Moderate RAM
@@ -314,19 +221,19 @@ CONFIG_SCHEMA = cv.All(
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS), cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
_consume_api_sockets, _validate_api_config,
) )
@coroutine_with_priority(CoroPriority.WEB) @coroutine_with_priority(CoroPriority.WEB)
async def to_code(config: ConfigType) -> None: async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
# Track controller registration for StaticVector sizing
CORE.register_controller()
cg.add(var.set_port(config[CONF_PORT])) cg.add(var.set_port(config[CONF_PORT]))
if config[CONF_PASSWORD]:
cg.add_define("USE_API_PASSWORD")
cg.add(var.set_password(config[CONF_PASSWORD]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
if CONF_LISTEN_BACKLOG in config: if CONF_LISTEN_BACKLOG in config:
@@ -335,13 +242,9 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS])) cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE]) cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled # Set USE_API_SERVICES if any services are enabled
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_USER_DEFINED_ACTIONS") cg.add_define("USE_API_SERVICES")
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
if config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_CUSTOM_SERVICES")
if config[CONF_HOMEASSISTANT_SERVICES]: if config[CONF_HOMEASSISTANT_SERVICES]:
cg.add_define("USE_API_HOMEASSISTANT_SERVICES") cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
@@ -350,66 +253,21 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("USE_API_HOMEASSISTANT_STATES") cg.add_define("USE_API_HOMEASSISTANT_STATES")
if actions := config.get(CONF_ACTIONS, []): if actions := config.get(CONF_ACTIONS, []):
# Collect all triggers first, then register all at once with initializer_list
triggers: list[cg.Pvariable] = []
for conf in actions: for conf in actions:
func_args: list[tuple[MockObj, str]] = [] template_args = []
service_template_args: list[MockObj] = [] # User service argument types func_args = []
service_arg_names = []
# Determine supports_response mode
# cv.enum returns the key with enum_value attribute containing the MockObj
supports_response_key = conf[CONF_SUPPORTS_RESPONSE]
supports_response = supports_response_key.enum_value
is_none = supports_response_key == "none"
is_optional = supports_response_key == "optional"
# Add call_id and return_response based on supports_response mode
# These must match the C++ Trigger template arguments
# - none: no extra args
# - status: call_id only (for reporting success/error without data)
# - only: call_id only (response always expected with data)
# - optional: call_id + return_response (client decides)
if not is_none:
# call_id is present for "optional", "only", and "status"
func_args.append((cg.uint32, "call_id"))
# return_response only present for "optional"
if is_optional:
func_args.append((cg.bool_, "return_response"))
service_arg_names: list[str] = []
for name, var_ in conf[CONF_VARIABLES].items(): for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_] native = SERVICE_ARG_NATIVE_TYPES[var_]
service_template_args.append(native) template_args.append(native)
func_args.append((native, name)) func_args.append((native, name))
service_arg_names.append(name) service_arg_names.append(name)
# Template args: supports_response mode, then user service arg types templ = cg.TemplateArguments(*template_args)
templ = cg.TemplateArguments(supports_response, *service_template_args)
trigger = cg.new_Pvariable( trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
templ,
conf[CONF_ACTION],
service_arg_names,
) )
triggers.append(trigger) cg.add(var.register_user_service(trigger))
auto = await automation.build_automation(trigger, func_args, conf) await automation.build_automation(trigger, func_args, conf)
# For non-none response modes, automatically append unregister action
# This ensures the call is unregistered after all actions complete (including async ones)
if not is_none:
arg_types = [arg[0] for arg in func_args]
action_templ = cg.TemplateArguments(*arg_types)
unregister_id = ID(
f"{conf[CONF_TRIGGER_ID]}__unregister",
is_declaration=True,
type=APIUnregisterServiceCallAction.template(action_templ),
)
unregister_action = cg.new_Pvariable(
unregister_id,
var,
)
cg.add(auto.add_actions([unregister_action]))
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
if CONF_ON_CLIENT_CONNECTED in config: if CONF_ON_CLIENT_CONNECTED in config:
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
@@ -653,98 +511,9 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
return var return var
CONF_SUCCESS = "success" @automation.register_condition("api.connected", APIConnectedCondition, {})
CONF_ERROR_MESSAGE = "error_message"
def _validate_api_respond_data(config):
"""Set flag during validation so AUTO_LOAD can include json component."""
if CONF_DATA in config:
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
return config
API_RESPOND_ACTION_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.use_id(APIServer),
cv.Optional(CONF_SUCCESS, default=True): cv.templatable(cv.boolean),
cv.Optional(CONF_ERROR_MESSAGE, default=""): cv.templatable(cv.string),
cv.Optional(CONF_DATA): cv.lambda_,
}
),
_validate_api_respond_data,
)
@automation.register_action(
"api.respond",
APIRespondAction,
API_RESPOND_ACTION_SCHEMA,
)
async def api_respond_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
# Validate that api.respond is used inside an API action context.
# We can't easily validate this at config time since the schema validation
# doesn't have access to the parent action context. Validating here in to_code
# is still much better than a cryptic C++ compile error.
has_call_id = any(name == "call_id" for _, name in args)
if not has_call_id:
raise EsphomeError(
"api.respond can only be used inside an API action's 'then:' block. "
"The 'call_id' variable is required to send a response."
)
cg.add_define("USE_API_USER_DEFINED_ACTION_RESPONSES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv)
# Check if we're in optional mode (has return_response arg)
is_optional = any(name == "return_response" for _, name in args)
if is_optional:
cg.add(var.set_is_optional_mode(True))
templ = await cg.templatable(config[CONF_SUCCESS], args, cg.bool_)
cg.add(var.set_success(templ))
templ = await cg.templatable(config[CONF_ERROR_MESSAGE], args, cg.std_string)
cg.add(var.set_error_message(templ))
if CONF_DATA in config:
cg.add_define("USE_API_USER_DEFINED_ACTION_RESPONSES_JSON")
# Lambda populates the JsonObject root - no return value needed
lambda_ = await cg.process_lambda(
config[CONF_DATA],
args + [(cg.JsonObject, "root")],
return_type=cg.void,
)
cg.add(var.set_data(lambda_))
return var
API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(APIServer),
cv.Optional(CONF_STATE_SUBSCRIPTION_ONLY, default=False): cv.templatable(
cv.boolean
),
}
)
@automation.register_condition(
"api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA
)
async def api_connected_to_code(config, condition_id, template_arg, args): async def api_connected_to_code(config, condition_id, template_arg, args):
var = cg.new_Pvariable(condition_id, template_arg) return cg.new_Pvariable(condition_id, template_arg)
templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_)
cg.add(var.set_state_subscription_only(templ))
return var
def FILTER_SOURCE_FILES() -> list[str]: def FILTER_SOURCE_FILES() -> list[str]:

View File

@@ -7,7 +7,10 @@ service APIConnection {
option (needs_setup_connection) = false; option (needs_setup_connection) = false;
option (needs_authentication) = false; option (needs_authentication) = false;
} }
// REMOVED in ESPHome 2026.1.0: rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse) rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse) {
option (needs_setup_connection) = false;
option (needs_authentication) = false;
}
rpc disconnect (DisconnectRequest) returns (DisconnectResponse) { rpc disconnect (DisconnectRequest) returns (DisconnectResponse) {
option (needs_setup_connection) = false; option (needs_setup_connection) = false;
option (needs_authentication) = false; option (needs_authentication) = false;
@@ -66,8 +69,6 @@ service APIConnection {
rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {} rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {}
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {} rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
} }
@@ -81,13 +82,14 @@ service APIConnection {
// * VarInt denoting the type of message. // * VarInt denoting the type of message.
// * The message object encoded as a ProtoBuf message // * The message object encoded as a ProtoBuf message
// The connection is established in 2 steps: // The connection is established in 4 steps:
// * First, the client connects to the server and sends a "Hello Request" identifying itself // * First, the client connects to the server and sends a "Hello Request" identifying itself
// * The server responds with a "Hello Response" and the connection is authenticated // * The server responds with a "Hello Response" and selects the protocol version
// * After receiving this message, the client attempts to authenticate itself using
// the password and a "Connect Request"
// * The server responds with a "Connect Response" and notifies of invalid password.
// If anything in this initial process fails, the connection must immediately closed // If anything in this initial process fails, the connection must immediately closed
// by both sides and _no_ disconnection message is to be sent. // by both sides and _no_ disconnection message is to be sent.
// Note: Password authentication via AuthenticationRequest/AuthenticationResponse (message IDs 3, 4)
// was removed in ESPHome 2026.1.0. Those message IDs are reserved and should not be reused.
// Message sent at the beginning of each connection // Message sent at the beginning of each connection
// Can only be sent by the client and only at the beginning of the connection // Can only be sent by the client and only at the beginning of the connection
@@ -100,7 +102,7 @@ message HelloRequest {
// For example "Home Assistant" // For example "Home Assistant"
// Not strictly necessary to send but nice for debugging // Not strictly necessary to send but nice for debugging
// purposes. // purposes.
string client_info = 1; string client_info = 1 [(pointer_to_buffer) = true];
uint32 api_version_major = 2; uint32 api_version_major = 2;
uint32 api_version_minor = 3; uint32 api_version_minor = 3;
} }
@@ -128,23 +130,25 @@ message HelloResponse {
string name = 4; string name = 4;
} }
// DEPRECATED in ESPHome 2026.1.0 - Password authentication is no longer supported. // Message sent at the beginning of each connection to authenticate the client
// These messages are kept for protocol documentation but are not processed by the server. // Can only be sent by the client and only at the beginning of the connection
// Use noise encryption instead: https://esphome.io/components/api/#configuration-variables
message AuthenticationRequest { message AuthenticationRequest {
option (id) = 3; option (id) = 3;
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (no_delay) = true; option (no_delay) = true;
option deprecated = true; option (ifdef) = "USE_API_PASSWORD";
string password = 1; // The password to log in with
string password = 1 [(pointer_to_buffer) = true];
} }
// Confirmation of successful connection. After this the connection is available for all traffic.
// Can only be sent by the server and only at the beginning of the connection
message AuthenticationResponse { message AuthenticationResponse {
option (id) = 4; option (id) = 4;
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (no_delay) = true; option (no_delay) = true;
option deprecated = true; option (ifdef) = "USE_API_PASSWORD";
bool invalid_password = 1; bool invalid_password = 1;
} }
@@ -201,9 +205,7 @@ message DeviceInfoResponse {
option (id) = 10; option (id) = 10;
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
// Deprecated in ESPHome 2026.1.0, but kept for backward compatibility bool uses_password = 1 [(field_ifdef) = "USE_API_PASSWORD"];
// with older ESPHome versions that still send this field.
bool uses_password = 1 [deprecated = true];
// The name of the node, given by "App.set_name()" // The name of the node, given by "App.set_name()"
string name = 2; string name = 2;
@@ -423,7 +425,7 @@ message ListEntitiesFanResponse {
bool disabled_by_default = 9; bool disabled_by_default = 9;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 11; EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"]; repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"];
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
} }
// Deprecated in API version 1.6 - only used in deprecated fields // Deprecated in API version 1.6 - only used in deprecated fields
@@ -516,7 +518,7 @@ message ListEntitiesLightResponse {
bool legacy_supports_color_temperature = 8 [deprecated=true]; bool legacy_supports_color_temperature = 8 [deprecated=true];
float min_mireds = 9; float min_mireds = 9;
float max_mireds = 10; float max_mireds = 10;
repeated string effects = 11 [(container_pointer_no_template) = "FixedVector<const char *>"]; repeated string effects = 11;
bool disabled_by_default = 13; bool disabled_by_default = 13;
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 15; EntityCategory entity_category = 15;
@@ -587,7 +589,6 @@ enum SensorStateClass {
STATE_CLASS_MEASUREMENT = 1; STATE_CLASS_MEASUREMENT = 1;
STATE_CLASS_TOTAL_INCREASING = 2; STATE_CLASS_TOTAL_INCREASING = 2;
STATE_CLASS_TOTAL = 3; STATE_CLASS_TOTAL = 3;
STATE_CLASS_MEASUREMENT_ANGLE = 4;
} }
// Deprecated in API version 1.5 // Deprecated in API version 1.5
@@ -765,7 +766,7 @@ message SubscribeHomeassistantServicesRequest {
message HomeassistantServiceMap { message HomeassistantServiceMap {
string key = 1; string key = 1;
string value = 2; string value = 2 [(no_zero_copy) = true];
} }
message HomeassistantActionRequest { message HomeassistantActionRequest {
@@ -781,7 +782,7 @@ message HomeassistantActionRequest {
bool is_event = 5; bool is_event = 5;
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"]; uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
string response_template = 8 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
} }
// Message sent by Home Assistant to ESPHome with service call response data // Message sent by Home Assistant to ESPHome with service call response data
@@ -794,7 +795,7 @@ message HomeassistantActionResponse {
uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
bool success = 2; // Whether the service call succeeded bool success = 2; // Whether the service call succeeded
string error_message = 3; // Error message if success = false string error_message = 3; // Error message if success = false
bytes response_data = 4 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
} }
// ==================== IMPORT HOME ASSISTANT STATES ==================== // ==================== IMPORT HOME ASSISTANT STATES ====================
@@ -839,7 +840,7 @@ message GetTimeResponse {
option (no_delay) = true; option (no_delay) = true;
fixed32 epoch_seconds = 1; fixed32 epoch_seconds = 1;
string timezone = 2; string timezone = 2 [(pointer_to_buffer) = true];
} }
// ==================== USER-DEFINES SERVICES ==================== // ==================== USER-DEFINES SERVICES ====================
@@ -853,31 +854,22 @@ enum ServiceArgType {
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6; SERVICE_ARG_TYPE_FLOAT_ARRAY = 6;
SERVICE_ARG_TYPE_STRING_ARRAY = 7; SERVICE_ARG_TYPE_STRING_ARRAY = 7;
} }
enum SupportsResponseType {
SUPPORTS_RESPONSE_NONE = 0;
SUPPORTS_RESPONSE_OPTIONAL = 1;
SUPPORTS_RESPONSE_ONLY = 2;
// Status-only response - reports success/error without data payload
// Value is higher to avoid conflicts with future Home Assistant values
SUPPORTS_RESPONSE_STATUS = 100;
}
message ListEntitiesServicesArgument { message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; option (ifdef) = "USE_API_SERVICES";
string name = 1; string name = 1;
ServiceArgType type = 2; ServiceArgType type = 2;
} }
message ListEntitiesServicesResponse { message ListEntitiesServicesResponse {
option (id) = 41; option (id) = 41;
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; option (ifdef) = "USE_API_SERVICES";
string name = 1; string name = 1;
fixed32 key = 2; fixed32 key = 2;
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true]; repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
} }
message ExecuteServiceArgument { message ExecuteServiceArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; option (ifdef) = "USE_API_SERVICES";
bool bool_ = 1; bool bool_ = 1;
int32 legacy_int = 2; int32 legacy_int = 2;
float float_ = 3; float float_ = 3;
@@ -893,25 +885,10 @@ message ExecuteServiceRequest {
option (id) = 42; option (id) = 42;
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (no_delay) = true; option (no_delay) = true;
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; option (ifdef) = "USE_API_SERVICES";
fixed32 key = 1; fixed32 key = 1;
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true]; repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
}
// Message sent by ESPHome to Home Assistant with service execution response data
message ExecuteServiceResponse {
option (id) = 131;
option (source) = SOURCE_SERVER;
option (no_delay) = true;
option (ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES";
uint32 call_id = 1; // Matches the call_id from ExecuteServiceRequest
bool success = 2; // Whether the service execution succeeded
string error_message = 3; // Error message if success = false
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES_JSON"];
} }
// ==================== CAMERA ==================== // ==================== CAMERA ====================
@@ -1012,7 +989,7 @@ message ListEntitiesClimateResponse {
bool supports_current_temperature = 5; // Deprecated: use feature_flags bool supports_current_temperature = 5; // Deprecated: use feature_flags
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"]; repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
float visual_min_temperature = 8; float visual_min_temperature = 8;
float visual_max_temperature = 9; float visual_max_temperature = 9;
float visual_target_temperature_step = 10; float visual_target_temperature_step = 10;
@@ -1021,11 +998,11 @@ message ListEntitiesClimateResponse {
// Deprecated in API version 1.5 // Deprecated in API version 1.5
bool legacy_supports_away = 11 [deprecated=true]; bool legacy_supports_away = 11 [deprecated=true];
bool supports_action = 12; // Deprecated: use feature_flags bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector<const char *>"]; repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"];
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"]; repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"];
bool disabled_by_default = 18; bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20; EntityCategory entity_category = 20;
@@ -1099,85 +1076,6 @@ message ClimateCommandRequest {
uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== WATER_HEATER ====================
enum WaterHeaterMode {
WATER_HEATER_MODE_OFF = 0;
WATER_HEATER_MODE_ECO = 1;
WATER_HEATER_MODE_ELECTRIC = 2;
WATER_HEATER_MODE_PERFORMANCE = 3;
WATER_HEATER_MODE_HIGH_DEMAND = 4;
WATER_HEATER_MODE_HEAT_PUMP = 5;
WATER_HEATER_MODE_GAS = 6;
}
message ListEntitiesWaterHeaterResponse {
option (id) = 132;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
float min_temperature = 8;
float max_temperature = 9;
float target_temperature_step = 10;
repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"];
// Bitmask of WaterHeaterFeature flags
uint32 supported_features = 12;
}
message WaterHeaterStateResponse {
option (id) = 133;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
fixed32 key = 1;
float current_temperature = 2;
float target_temperature = 3;
WaterHeaterMode mode = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// Bitmask of current state flags (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// Bitmask for WaterHeaterCommandRequest.has_fields
enum WaterHeaterCommandHasField {
WATER_HEATER_COMMAND_HAS_NONE = 0;
WATER_HEATER_COMMAND_HAS_MODE = 1;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
WATER_HEATER_COMMAND_HAS_STATE = 4;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
}
message WaterHeaterCommandRequest {
option (id) = 134;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
// Bitmask of which fields are set (see WaterHeaterCommandHasField)
uint32 has_fields = 2;
WaterHeaterMode mode = 3;
float target_temperature = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// State flags bitmask (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// ==================== NUMBER ==================== // ==================== NUMBER ====================
enum NumberMode { enum NumberMode {
NUMBER_MODE_AUTO = 0; NUMBER_MODE_AUTO = 0;
@@ -1245,7 +1143,7 @@ message ListEntitiesSelectResponse {
reserved 4; // Deprecated: was string unique_id reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"]; repeated string options = 6 [(container_pointer) = "std::vector"];
bool disabled_by_default = 7; bool disabled_by_default = 7;
EntityCategory entity_category = 8; EntityCategory entity_category = 8;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
@@ -1290,7 +1188,7 @@ message ListEntitiesSirenResponse {
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6; bool disabled_by_default = 6;
repeated string tones = 7 [(container_pointer_no_template) = "FixedVector<const char *>"]; repeated string tones = 7;
bool supports_duration = 8; bool supports_duration = 8;
bool supports_volume = 9; bool supports_volume = 9;
EntityCategory entity_category = 10; EntityCategory entity_category = 10;
@@ -1690,7 +1588,7 @@ message BluetoothGATTWriteRequest {
uint32 handle = 2; uint32 handle = 2;
bool response = 3; bool response = 3;
bytes data = 4; bytes data = 4 [(pointer_to_buffer) = true];
} }
message BluetoothGATTReadDescriptorRequest { message BluetoothGATTReadDescriptorRequest {
@@ -1710,7 +1608,7 @@ message BluetoothGATTWriteDescriptorRequest {
uint64 address = 1; uint64 address = 1;
uint32 handle = 2; uint32 handle = 2;
bytes data = 3; bytes data = 3 [(pointer_to_buffer) = true];
} }
message BluetoothGATTNotifyRequest { message BluetoothGATTNotifyRequest {
@@ -1935,7 +1833,7 @@ message VoiceAssistantAudio {
option (source) = SOURCE_BOTH; option (source) = SOURCE_BOTH;
option (ifdef) = "USE_VOICE_ASSISTANT"; option (ifdef) = "USE_VOICE_ASSISTANT";
bytes data = 1 [(pointer_to_buffer) = true]; bytes data = 1;
bool end = 2; bool end = 2;
} }
@@ -2249,7 +2147,7 @@ message ListEntitiesEventResponse {
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
string device_class = 8; string device_class = 8;
repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector<const char *>"]; repeated string event_types = 9;
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
} }
message EventResponse { message EventResponse {
@@ -2423,7 +2321,7 @@ message ZWaveProxyFrame {
option (ifdef) = "USE_ZWAVE_PROXY"; option (ifdef) = "USE_ZWAVE_PROXY";
option (no_delay) = true; option (no_delay) = true;
bytes data = 1; bytes data = 1 [(pointer_to_buffer) = true];
} }
enum ZWaveProxyRequestType { enum ZWaveProxyRequestType {
@@ -2437,51 +2335,5 @@ message ZWaveProxyRequest {
option (ifdef) = "USE_ZWAVE_PROXY"; option (ifdef) = "USE_ZWAVE_PROXY";
ZWaveProxyRequestType type = 1; ZWaveProxyRequestType type = 1;
bytes data = 2; bytes data = 2 [(pointer_to_buffer) = true];
}
// ==================== INFRARED ====================
// Note: Feature and capability flag enums are defined in
// esphome/components/infrared/infrared.h
// Listing of infrared instances
message ListEntitiesInfraredResponse {
option (id) = 135;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_INFRARED";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
uint32 capabilities = 8; // Bitfield of InfraredCapabilityFlags
}
// Command to transmit infrared/RF data using raw timings
message InfraredRFTransmitRawTimingsRequest {
option (id) = 136;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_IR_RF";
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2; // Key identifying the transmitter instance
uint32 carrier_frequency = 3; // Carrier frequency in Hz
uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.)
repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off)
}
// Event message for received infrared/RF data
message InfraredRFReceiveEvent {
option (id) = 137;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_IR_RF";
option (no_delay) = true;
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2; // Key identifying the receiver instance
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
} }

File diff suppressed because it is too large Load Diff

View File

@@ -9,23 +9,32 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/string_ref.h"
#include <functional> #include <functional>
#include <vector> #include <vector>
namespace esphome::api { namespace esphome::api {
// Client information structure
struct ClientInfo {
std::string name; // Client name from Hello message
std::string peername; // IP:port from socket
};
// Keepalive timeout in milliseconds // Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending // Maximum number of entities to process in a single batch during initial state/info sending
// API 1.14+ clients compute object_id client-side, so messages are smaller and we can fit more per batch // This was increased from 20 to 24 after removing the unique_id field from entity info messages,
// TODO: Remove MAX_INITIAL_PER_BATCH_LEGACY before 2026.7.0 - all clients should support API 1.14 by then // which reduced message sizes allowing more entities per batch without exceeding packet limits
static constexpr size_t MAX_INITIAL_PER_BATCH_LEGACY = 24; // For clients < API 1.14 (includes object_id) static constexpr size_t MAX_INITIAL_PER_BATCH = 24;
static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= API 1.14 (no object_id) // Maximum number of packets to process in a single batch (platform-dependent)
// Verify MAX_MESSAGES_PER_BATCH (defined in api_frame_helper.h) can hold the initial batch // This limit exists to prevent stack overflow from the PacketInfo array in process_batch_
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH, // Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH"); #if defined(USE_ESP32) || defined(USE_HOST)
static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HOST has plenty
#else
static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks
#endif
class APIConnection final : public APIServerConnection { class APIConnection final : public APIServerConnection {
public: public:
@@ -167,18 +176,8 @@ class APIConnection final : public APIServerConnection {
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif #endif
#ifdef USE_WATER_HEATER
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif
#ifdef USE_IR_RF
void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override;
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
void send_event(event::Event *event, StringRef event_type); void send_event(event::Event *event, const std::string &event_type);
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
@@ -198,17 +197,16 @@ class APIConnection final : public APIServerConnection {
void on_get_time_response(const GetTimeResponse &value) override; void on_get_time_response(const GetTimeResponse &value) override;
#endif #endif
bool send_hello_response(const HelloRequest &msg) override; bool send_hello_response(const HelloRequest &msg) override;
#ifdef USE_API_PASSWORD
bool send_authenticate_response(const AuthenticationRequest &msg) override;
#endif
bool send_disconnect_response(const DisconnectRequest &msg) override; bool send_disconnect_response(const DisconnectRequest &msg) override;
bool send_ping_response(const PingRequest &msg) override; bool send_ping_response(const PingRequest &msg) override;
bool send_device_info_response(const DeviceInfoRequest &msg) override; bool send_device_info_response(const DeviceInfoRequest &msg) override;
void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
void subscribe_states(const SubscribeStatesRequest &msg) override { void subscribe_states(const SubscribeStatesRequest &msg) override {
this->flags_.state_subscription = true; this->flags_.state_subscription = true;
// Start initial state iterator only if no iterator is active this->initial_state_iterator_.begin();
// If list_entities is running, we'll start initial_state when it completes
if (this->active_iterator_ == ActiveIterator::NONE) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
} }
void subscribe_logs(const SubscribeLogsRequest &msg) override { void subscribe_logs(const SubscribeLogsRequest &msg) override {
this->flags_.log_subscription = msg.level; this->flags_.log_subscription = msg.level;
@@ -223,15 +221,8 @@ class APIConnection final : public APIServerConnection {
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
void execute_service(const ExecuteServiceRequest &msg) override; void execute_service(const ExecuteServiceRequest &msg) override;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message,
const uint8_t *response_data, size_t response_data_len);
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override; bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
@@ -253,6 +244,9 @@ class APIConnection final : public APIServerConnection {
} }
void on_fatal_error() override; void on_fatal_error() override;
#ifdef USE_API_PASSWORD
void on_unauthenticated_access() override;
#endif
void on_no_setup_connection() override; void on_no_setup_connection() override;
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
// FIXME: ensure no recursive writes can happen // FIXME: ensure no recursive writes can happen
@@ -279,18 +273,13 @@ class APIConnection final : public APIServerConnection {
bool try_to_clear_buffer(bool log_out_of_space); bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const char *get_name() const { return this->helper_->get_client_name(); } const std::string &get_name() const { return this->client_info_.name; }
/// Get peer name (IP address) - cached at connection init time const std::string &get_peername() const { return this->client_info_.peername; }
const char *get_peername() const { return this->helper_->get_client_peername(); }
protected: protected:
// Helper function to handle authentication completion // Helper function to handle authentication completion
void complete_authentication_(); void complete_authentication_();
#ifdef USE_CAMERA
void try_send_camera_image_();
#endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void process_state_subscriptions_(); void process_state_subscriptions_();
#endif #endif
@@ -314,24 +303,25 @@ class APIConnection final : public APIServerConnection {
APIConnection *conn, uint32_t remaining_size, bool is_single) { APIConnection *conn, uint32_t remaining_size, bool is_single) {
// Set common fields that are shared by all entity types // Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash(); msg.key = entity->get_object_id_hash();
// Try to use static reference first to avoid allocation
// API 1.14+ clients compute object_id client-side from the entity name StringRef static_ref = entity->get_object_id_ref_for_api_();
// For older clients, we must send object_id for backward compatibility // Store dynamic string outside the if-else to maintain lifetime
// See: https://github.com/esphome/backlog/issues/76 std::string object_id;
// TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then if (!static_ref.empty()) {
// Buffer must remain in scope until encode_message_to_buffer is called msg.set_object_id(static_ref);
char object_id_buf[OBJECT_ID_MAX_LEN]; } else {
if (!conn->client_supports_api_version(1, 14)) { // Dynamic case - need to allocate
msg.object_id = entity->get_object_id_to(object_id_buf); object_id = entity->get_object_id();
msg.set_object_id(StringRef(object_id));
} }
if (entity->has_own_name()) { if (entity->has_own_name()) {
msg.name = entity->get_name(); msg.set_name(entity->get_name());
} }
// Set common EntityBase properties // Set common EntityBase properties
#ifdef USE_ENTITY_ICON #ifdef USE_ENTITY_ICON
msg.icon = entity->get_icon_ref(); msg.set_icon(entity->get_icon_ref());
#endif #endif
msg.disabled_by_default = entity->is_disabled_by_default(); msg.disabled_by_default = entity->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
@@ -346,24 +336,16 @@ class APIConnection final : public APIServerConnection {
inline bool check_voice_assistant_api_connection_() const; inline bool check_voice_assistant_api_connection_() const;
#endif #endif
// Get the max batch size based on client API version
// API 1.14+ clients don't receive object_id, so messages are smaller and more fit per batch
// TODO: Remove this method before 2026.7.0 and use MAX_INITIAL_PER_BATCH directly
size_t get_max_batch_size_() const {
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
}
// Helper method to process multiple entities from an iterator in a batch // Helper method to process multiple entities from an iterator in a batch
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) { template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
size_t initial_size = this->deferred_batch_.size(); size_t initial_size = this->deferred_batch_.size();
size_t max_batch = this->get_max_batch_size_(); while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) {
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
iterator.advance(); iterator.advance();
} }
// If the batch is full, process it immediately // If the batch is full, process it immediately
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_() // Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
if (this->deferred_batch_.size() >= max_batch) { if (this->deferred_batch_.size() >= MAX_INITIAL_PER_BATCH) {
this->process_batch_(); this->process_batch_();
} }
} }
@@ -467,18 +449,8 @@ class APIConnection final : public APIServerConnection {
static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single); bool is_single);
#endif #endif
#ifdef USE_WATER_HEATER
static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_INFRARED
static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single); uint32_t remaining_size, bool is_single);
static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
#endif #endif
@@ -511,27 +483,18 @@ class APIConnection final : public APIServerConnection {
std::unique_ptr<APIFrameHelper> helper_; std::unique_ptr<APIFrameHelper> helper_;
APIServer *parent_; APIServer *parent_;
// Group 2: Iterator union (saves ~16 bytes vs separate iterators) // Group 2: Larger objects (must be 4-byte aligned)
// These iterators are never active simultaneously - list_entities runs to completion // These contain vectors/pointers internally, so putting them early ensures good alignment
// before initial_state begins, so we use a union with explicit construction/destruction. InitialStateIterator initial_state_iterator_;
enum class ActiveIterator : uint8_t { NONE, LIST_ENTITIES, INITIAL_STATE }; ListEntitiesIterator list_entities_iterator_;
union IteratorUnion {
ListEntitiesIterator list_entities;
InitialStateIterator initial_state;
// Constructor/destructor do nothing - use placement new/explicit destructor
IteratorUnion() {}
~IteratorUnion() {}
} iterator_storage_;
// Helper methods for iterator lifecycle management
void destroy_active_iterator_();
void begin_iterator_(ActiveIterator type);
#ifdef USE_CAMERA #ifdef USE_CAMERA
std::unique_ptr<camera::CameraImageReader> image_reader_; std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif #endif
// Group 3: 4-byte types // Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each)
ClientInfo client_info_;
// Group 4: 4-byte types
uint32_t last_traffic_; uint32_t last_traffic_;
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
int state_subs_at_ = -1; int state_subs_at_ = -1;
@@ -542,18 +505,51 @@ class APIConnection final : public APIServerConnection {
class MessageCreator { class MessageCreator {
public: public:
// Constructor for function pointer
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; } MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
// Constructor for string state capture
explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); }
// No destructor - cleanup must be called explicitly with message_type
// Delete copy operations - MessageCreator should only be moved
MessageCreator(const MessageCreator &other) = delete;
MessageCreator &operator=(const MessageCreator &other) = delete;
// Move constructor
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; }
// Move assignment
MessageCreator &operator=(MessageCreator &&other) noexcept {
if (this != &other) {
// IMPORTANT: Caller must ensure cleanup() was called if this contains a string!
// In our usage, this happens in add_item() deduplication and vector::erase()
data_ = other.data_;
other.data_.function_ptr = nullptr;
}
return *this;
}
// Call operator - uses message_type to determine union type // Call operator - uses message_type to determine union type
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint8_t message_type) const; uint8_t message_type) const;
// Manual cleanup method - must be called before destruction for string types
void cleanup(uint8_t message_type) {
#ifdef USE_EVENT
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
delete data_.string_ptr;
data_.string_ptr = nullptr;
}
#endif
}
private: private:
union Data { union Data {
MessageCreatorPtr function_ptr; MessageCreatorPtr function_ptr;
const char *const_char_ptr; std::string *string_ptr;
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
}; };
// Generic batching mechanism for both state updates and entity info // Generic batching mechanism for both state updates and entity info
@@ -566,41 +562,52 @@ class APIConnection final : public APIServerConnection {
// Constructor for creating BatchItem // Constructor for creating BatchItem
BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size)
: entity(entity), creator(creator), message_type(message_type), estimated_size(estimated_size) {} : entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {}
}; };
std::vector<BatchItem> items; std::vector<BatchItem> items;
uint32_t batch_start_time{0}; uint32_t batch_start_time{0};
// No pre-allocation - log connections never use batching, and for private:
// connections that do, buffers are released after initial sync anyway // Helper to cleanup items from the beginning
void cleanup_items_(size_t count) {
for (size_t i = 0; i < count; i++) {
items[i].creator.cleanup(items[i].message_type);
}
}
public:
DeferredBatch() {
// Pre-allocate capacity for typical batch sizes to avoid reallocation
items.reserve(8);
}
~DeferredBatch() {
// Ensure cleanup of any remaining items
clear();
}
// Add item to the batch // Add item to the batch
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
// Add item to the front of the batch (for high priority messages like ping) // Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
// Clear all items // Clear all items with proper cleanup
void clear() { void clear() {
cleanup_items_(items.size());
items.clear(); items.clear();
batch_start_time = 0; batch_start_time = 0;
} }
// Remove processed items from the front // Remove processed items from the front with proper cleanup
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } void remove_front(size_t count) {
cleanup_items_(count);
items.erase(items.begin(), items.begin() + count);
}
bool empty() const { return items.empty(); } bool empty() const { return items.empty(); }
size_t size() const { return items.size(); } size_t size() const { return items.size(); }
const BatchItem &operator[](size_t index) const { return items[index]; } const BatchItem &operator[](size_t index) const { return items[index]; }
// Release excess capacity - only releases if items already empty
void release_buffer() {
// Safe to call: batch is processed before release_buffer is called,
// and if any items remain (partial processing), we must not clear them.
// Use swap trick since shrink_to_fit() is non-binding and may be ignored.
if (items.empty()) {
std::vector<BatchItem>().swap(items);
}
}
}; };
// DeferredBatch here (16 bytes, 4-byte aligned) // DeferredBatch here (16 bytes, 4-byte aligned)
@@ -638,9 +645,7 @@ class APIConnection final : public APIServerConnection {
// 2-byte types immediately after flags_ (no padding between them) // 2-byte types immediately after flags_ (no padding between them)
uint16_t client_api_version_major_{0}; uint16_t client_api_version_major_{0};
uint16_t client_api_version_minor_{0}; uint16_t client_api_version_minor_{0};
// 1-byte type to fill padding // Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary
ActiveIterator active_iterator_{ActiveIterator::NONE};
// Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary
uint32_t get_batch_delay_ms_() const; uint32_t get_batch_delay_ms_() const;
// Message will use 8 more bytes than the minimum size, and typical // Message will use 8 more bytes than the minimum size, and typical
@@ -677,30 +682,21 @@ class APIConnection final : public APIServerConnection {
} }
#endif #endif
// Helper to check if a message type should bypass batching
// Returns true if:
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
// the main loop is blocked, e.g., during OTA updates)
// 2. It's an EventResponse (events are edge-triggered - every occurrence matters)
// 3. OR: User has opted into immediate sending (should_try_send_immediately = true
// AND batch_delay = 0)
inline bool should_send_immediately_(uint8_t message_type) const {
return (
#ifdef USE_UPDATE
message_type == UpdateStateResponse::MESSAGE_TYPE ||
#endif
#ifdef USE_EVENT
message_type == EventResponse::MESSAGE_TYPE ||
#endif
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0));
}
// Helper method to send a message either immediately or via batching // Helper method to send a message either immediately or via batching
// Tries immediate send if should_send_immediately_() returns true and buffer has space
// Falls back to batching if immediate send fails or isn't applicable
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
uint8_t estimated_size) { uint8_t estimated_size) {
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { // Try to send immediately if:
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
// the main loop is blocked, e.g., during OTA updates)
// 2. OR: We should try to send immediately (should_try_send_immediately = true)
// AND Batch delay is 0 (user has opted in to immediate sending)
// 3. AND: Buffer has space available
if ((
#ifdef USE_UPDATE
message_type == UpdateStateResponse::MESSAGE_TYPE ||
#endif
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) &&
this->helper_->can_write_without_blocking()) {
// Now actually encode and send // Now actually encode and send
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
@@ -718,30 +714,9 @@ class APIConnection final : public APIServerConnection {
return this->schedule_message_(entity, creator, message_type, estimated_size); return this->schedule_message_(entity, creator, message_type, estimated_size);
} }
// Overload for MessageCreator (used by events which need to capture event_type)
bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
// Try to send immediately if message type should bypass batching and buffer has space
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
// Now actually encode and send
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message in verbose mode
this->log_proto_message_(entity, creator, message_type);
#endif
return true;
}
// If immediate send failed, fall through to batching
}
// Fall back to scheduled batching
return this->schedule_message_(entity, creator, message_type, estimated_size);
}
// Helper function to schedule a deferred message with known message type // Helper function to schedule a deferred message with known message type
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
this->deferred_batch_.add_item(entity, creator, message_type, estimated_size); this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
return this->schedule_batch_(); return this->schedule_batch_();
} }
@@ -758,8 +733,6 @@ class APIConnection final : public APIServerConnection {
return this->schedule_batch_(); return this->schedule_batch_();
} }
// Helper function to log client messages with name and peername
void log_client_(int level, const LogString *message);
// Helper function to log API errors with errno // Helper function to log API errors with errno
void log_warning_(const LogString *message, APIError err); void log_warning_(const LogString *message, APIError err);
// Helper to handle fatal errors with logging // Helper to handle fatal errors with logging

View File

@@ -1,5 +1,6 @@
#include "api_frame_helper.h" #include "api_frame_helper.h"
#ifdef USE_API #ifdef USE_API
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -12,29 +13,12 @@ namespace esphome::api {
static const char *const TAG = "api.frame_helper"; static const char *const TAG = "api.frame_helper";
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) #define HELPER_LOG(msg, ...) \
static constexpr size_t API_MAX_LOG_BYTES = 168; ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) \ #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
do { \ #define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Received frame: %s", \
format_hex_pretty_to(hex_buf_, (buffer).data(), \
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_SENDING(data, len) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Sending raw: %s", \
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
} while (0)
#else #else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0) #define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0) #define LOG_PACKET_SENDING(data, len) ((void) 0)
@@ -245,8 +229,6 @@ APIError APIFrameHelper::init_common_() {
HELPER_LOG("Bad state for init %d", (int) state_); HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE; return APIError::BAD_STATE;
} }
// Cache peername now while socket is valid - needed for error logging after socket failure
this->socket_->getpeername_to(this->client_peername_);
int err = this->socket_->setblocking(false); int err = this->socket_->setblocking(false);
if (err != 0) { if (err != 0) {
state_ = State::FAILED; state_ = State::FAILED;

View File

@@ -29,28 +29,25 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
#endif #endif
// Maximum number of messages to batch in a single write operation // Forward declaration
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there) struct ClientInfo;
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
class ProtoWriteBuffer; class ProtoWriteBuffer;
// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32;
struct ReadPacketBuffer { struct ReadPacketBuffer {
const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call) std::vector<uint8_t> container;
uint16_t data_len;
uint16_t type; uint16_t type;
uint16_t data_offset;
uint16_t data_len;
}; };
// Packed message info structure to minimize memory usage // Packed packet info structure to minimize memory usage
struct MessageInfo { struct PacketInfo {
uint16_t offset; // Offset in buffer where message starts uint16_t offset; // Offset in buffer where message starts
uint16_t payload_size; // Size of the message payload uint16_t payload_size; // Size of the message payload
uint8_t message_type; // Message type (0-255) uint8_t message_type; // Message type (0-255)
MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {} PacketInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
}; };
enum class APIError : uint16_t { enum class APIError : uint16_t {
@@ -86,23 +83,16 @@ const LogString *api_error_to_logstr(APIError err);
class APIFrameHelper { class APIFrameHelper {
public: public:
APIFrameHelper() = default; APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {} explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: socket_owned_(std::move(socket)), client_info_(client_info) {
// Get client name (null-terminated) socket_ = socket_owned_.get();
const char *get_client_name() const { return this->client_name_; }
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
const char *get_client_peername() const { return this->client_peername_; }
// Set client name from buffer with length (truncates if needed)
void set_client_name(const char *name, size_t len) {
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
memcpy(this->client_name_, name, copy_len);
this->client_name_[copy_len] = '\0';
} }
virtual ~APIFrameHelper() = default; virtual ~APIFrameHelper() = default;
virtual APIError init() = 0; virtual APIError init() = 0;
virtual APIError loop(); virtual APIError loop();
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
std::string getpeername() { return socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() { APIError close() {
state_ = State::CLOSED; state_ = State::CLOSED;
@@ -120,48 +110,17 @@ class APIFrameHelper {
} }
return APIError::OK; return APIError::OK;
} }
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
///
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
/// state updates low-latency (NODELAY enabled). Without this, many small log
/// packets fill the TCP send buffer, crowding out important state updates.
///
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
///
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
/// @return true if successful or already in desired state
bool set_nodelay(bool enable) {
if (this->nodelay_enabled_ == enable)
return true;
int val = enable ? 1 : 0;
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
if (err == 0) {
this->nodelay_enabled_ = enable;
}
return err == 0;
}
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf messages in a single operation // Write multiple protobuf packets in a single operation
// messages contains (message_type, offset, length) for each message in the buffer // packets contains (message_type, offset, length) for each message in the buffer
// The buffer contains all messages with appropriate padding before each // The buffer contains all messages with appropriate padding before each
virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0; virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) = 0;
// Get the frame header padding required by this protocol // Get the frame header padding required by this protocol
uint8_t frame_header_padding() const { return frame_header_padding_; } uint8_t frame_header_padding() const { return frame_header_padding_; }
// Get the frame footer size required by this protocol // Get the frame footer size required by this protocol
uint8_t frame_footer_size() const { return frame_footer_size_; } uint8_t frame_footer_size() const { return frame_footer_size_; }
// Check if socket has data ready to read // Check if socket has data ready to read
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
// Release excess memory from internal buffers after initial sync
void release_buffers() {
// rx_buf_: Safe to clear only if no partial read in progress.
// rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
// and clearing would lose partially received data.
if (this->rx_buf_len_ == 0) {
// Use swap trick since shrink_to_fit() is non-binding and may be ignored
std::vector<uint8_t>().swap(this->rx_buf_);
}
}
protected: protected:
// Buffer containing data to be sent // Buffer containing data to be sent
@@ -190,8 +149,9 @@ class APIFrameHelper {
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf, APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state); const std::string &info, StateEnum &state, StateEnum failed_state);
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) // Pointers first (4 bytes each)
std::unique_ptr<socket::Socket> socket_; socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Common state enum for all frame helpers // Common state enum for all frame helpers
// Note: Not all states are used by all implementations // Note: Not all states are used by all implementations
@@ -214,12 +174,12 @@ class APIFrameHelper {
// Containers (size varies, but typically 12+ bytes on 32-bit) // Containers (size varies, but typically 12+ bytes on 32-bit)
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_; std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
std::vector<struct iovec> reusable_iovs_;
std::vector<uint8_t> rx_buf_; std::vector<uint8_t> rx_buf_;
// Client name buffer - stores name from Hello message or initial peername // Pointer to client info (4 bytes on 32-bit)
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; // Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance.
// Cached peername/IP address - captured at init time for availability after socket failure const ClientInfo *client_info_{nullptr};
char client_peername_[socket::SOCKADDR_STR_LEN]{};
// Group smaller types together // Group smaller types together
uint16_t rx_buf_len_ = 0; uint16_t rx_buf_len_ = 0;
@@ -229,10 +189,7 @@ class APIFrameHelper {
uint8_t tx_buf_head_{0}; uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0}; uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0}; uint8_t tx_buf_count_{0};
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true // 8 bytes total, 0 bytes padding
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
// messages to coalesce while keeping state updates low-latency.
bool nodelay_enabled_{true};
// Common initialization for both plaintext and noise protocols // Common initialization for both plaintext and noise protocols
APIError init_common_(); APIError init_common_();

View File

@@ -24,29 +24,12 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit";
#endif #endif
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) #define HELPER_LOG(msg, ...) \
static constexpr size_t API_MAX_LOG_BYTES = 168; ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) \ #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
do { \ #define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Received frame: %s", \
format_hex_pretty_to(hex_buf_, (buffer).data(), \
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_SENDING(data, len) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Sending raw: %s", \
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
} while (0)
#else #else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0) #define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0) #define LOG_PACKET_SENDING(data, len) ((void) 0)
@@ -256,13 +239,12 @@ APIError APINoiseFrameHelper::state_action_() {
} }
if (state_ == State::SERVER_HELLO) { if (state_ == State::SERVER_HELLO) {
// send server hello // send server hello
constexpr size_t mac_len = 13; // 12 hex chars + null terminator
const std::string &name = App.get_name(); const std::string &name = App.get_name();
char mac[mac_len]; const std::string &mac = get_mac_address();
get_mac_address_into_buffer(mac);
// Calculate positions and sizes // Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator size_t name_len = name.size() + 1; // including null terminator
size_t mac_len = mac.size() + 1; // including null terminator
size_t name_offset = 1; size_t name_offset = 1;
size_t mac_offset = name_offset + name_len; size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + mac_len; size_t total_size = 1 + name_len + mac_len;
@@ -275,7 +257,7 @@ APIError APINoiseFrameHelper::state_action_() {
// node name, terminated by null byte // node name, terminated by null byte
std::memcpy(msg.get() + name_offset, name.c_str(), name_len); std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte // node mac, terminated by null byte
std::memcpy(msg.get() + mac_offset, mac, mac_len); std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len);
aerr = write_frame_(msg.get(), total_size); aerr = write_frame_(msg.get(), total_size);
if (aerr != APIError::OK) if (aerr != APIError::OK)
@@ -424,7 +406,8 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
buffer->data = msg_data + 4; // Skip 4-byte header (type + length) buffer->container = std::move(this->rx_buf_);
buffer->data_offset = 4;
buffer->data_len = data_len; buffer->data_len = data_len;
buffer->type = type; buffer->type = type;
return APIError::OK; return APIError::OK;
@@ -432,12 +415,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize to include MAC space (required for Noise encryption) // Resize to include MAC space (required for Noise encryption)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
MessageInfo msg{type, 0, PacketInfo packet{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1)); return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
} }
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) { APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
APIError aerr = state_action_(); APIError aerr = state_action_();
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
return aerr; return aerr;
@@ -447,20 +430,21 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
if (messages.empty()) { if (packets.empty()) {
return APIError::OK; return APIError::OK;
} }
uint8_t *buffer_data = buffer.get_buffer()->data(); std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
// Stack-allocated iovec array - no heap allocation this->reusable_iovs_.clear();
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs; this->reusable_iovs_.reserve(packets.size());
uint16_t total_write_len = 0; uint16_t total_write_len = 0;
// We need to encrypt each message in place // We need to encrypt each packet in place
for (const auto &msg : messages) { for (const auto &packet : packets) {
// The buffer already has padding at offset // The buffer already has padding at offset
uint8_t *buf_start = buffer_data + msg.offset; uint8_t *buf_start = buffer_data + packet.offset;
// Write noise header // Write noise header
buf_start[0] = 0x01; // indicator buf_start[0] = 0x01; // indicator
@@ -468,10 +452,10 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
// Write message header (to be encrypted) // Write message header (to be encrypted)
const uint8_t msg_offset = 3; const uint8_t msg_offset = 3;
buf_start[msg_offset] = static_cast<uint8_t>(msg.message_type >> 8); // type high byte buf_start[msg_offset] = static_cast<uint8_t>(packet.message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(msg.message_type); // type low byte buf_start[msg_offset + 1] = static_cast<uint8_t>(packet.message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(msg.payload_size >> 8); // data_len high byte buf_start[msg_offset + 2] = static_cast<uint8_t>(packet.payload_size >> 8); // data_len high byte
buf_start[msg_offset + 3] = static_cast<uint8_t>(msg.payload_size); // data_len low byte buf_start[msg_offset + 3] = static_cast<uint8_t>(packet.payload_size); // data_len low byte
// payload data is already in the buffer starting at offset + 7 // payload data is already in the buffer starting at offset + 7
// Make sure we have space for MAC // Make sure we have space for MAC
@@ -480,8 +464,8 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
// Encrypt the message in place // Encrypt the message in place
NoiseBuffer mbuf; NoiseBuffer mbuf;
noise_buffer_init(mbuf); noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + msg.payload_size, noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size,
4 + msg.payload_size + frame_footer_size_); 4 + packet.payload_size + frame_footer_size_);
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
APIError aerr = APIError aerr =
@@ -493,14 +477,14 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8); buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size); buf_start[2] = static_cast<uint8_t>(mbuf.size);
// Add iovec for this encrypted message // Add iovec for this encrypted packet
size_t msg_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data size_t packet_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
iovs.push_back({buf_start, msg_len}); this->reusable_iovs_.push_back({buf_start, packet_len});
total_write_len += msg_len; total_write_len += packet_len;
} }
// Send all encrypted messages in one writev call // Send all encrypted packets in one writev call
return this->write_raw_(iovs.data(), iovs.size(), total_write_len); return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
} }
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
@@ -544,7 +528,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
const auto &psk = this->ctx_.get_psk(); const auto &psk = ctx_->get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"), aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
APIError::HANDSHAKESTATE_SETUP_FAILED); APIError::HANDSHAKESTATE_SETUP_FAILED);
@@ -556,8 +540,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now // set_prologue copies it into handshakestate, so we can get rid of it now
// Use swap idiom to actually release memory (= {} only clears size, not capacity) prologue_ = {};
std::vector<uint8_t>().swap(prologue_);
err = noise_handshakestate_start(handshake_); err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);

View File

@@ -9,8 +9,9 @@ namespace esphome::api {
class APINoiseFrameHelper final : public APIFrameHelper { class APINoiseFrameHelper final : public APIFrameHelper {
public: public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx) APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
: APIFrameHelper(std::move(socket)), ctx_(ctx) { const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
// Noise header structure: // Noise header structure:
// Pos 0: indicator (0x01) // Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian) // Pos 1-2: encrypted payload size (16-bit big-endian)
@@ -23,7 +24,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
APIError loop() override; APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override; APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override; APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
protected: protected:
APIError state_action_(); APIError state_action_();
@@ -40,8 +41,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
NoiseCipherState *send_cipher_{nullptr}; NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr}; NoiseCipherState *recv_cipher_{nullptr};
// Reference to noise context (4 bytes on 32-bit) // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
APINoiseContext &ctx_; std::shared_ptr<APINoiseContext> ctx_;
// Vector (12 bytes on 32-bit) // Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_; std::vector<uint8_t> prologue_;

View File

@@ -1,6 +1,7 @@
#include "api_frame_helper_plaintext.h" #include "api_frame_helper_plaintext.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_PLAINTEXT #ifdef USE_API_PLAINTEXT
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -17,29 +18,12 @@ namespace esphome::api {
static const char *const TAG = "api.plaintext"; static const char *const TAG = "api.plaintext";
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) #define HELPER_LOG(msg, ...) \
static constexpr size_t API_MAX_LOG_BYTES = 168; ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) \ #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
do { \ #define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Received frame: %s", \
format_hex_pretty_to(hex_buf_, (buffer).data(), \
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_SENDING(data, len) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Sending raw: %s", \
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
} while (0)
#else #else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0) #define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0) #define LOG_PACKET_SENDING(data, len) ((void) 0)
@@ -226,36 +210,37 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return aerr; return aerr;
} }
buffer->data = this->rx_buf_.data(); buffer->container = std::move(this->rx_buf_);
buffer->data_offset = 0;
buffer->data_len = this->rx_header_parsed_len_; buffer->data_len = this->rx_header_parsed_len_;
buffer->type = this->rx_header_parsed_type_; buffer->type = this->rx_header_parsed_type_;
return APIError::OK; return APIError::OK;
} }
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)}; PacketInfo packet{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1)); return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
} }
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
std::span<const MessageInfo> messages) {
if (state_ != State::DATA) { if (state_ != State::DATA) {
return APIError::BAD_STATE; return APIError::BAD_STATE;
} }
if (messages.empty()) { if (packets.empty()) {
return APIError::OK; return APIError::OK;
} }
uint8_t *buffer_data = buffer.get_buffer()->data(); std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
// Stack-allocated iovec array - no heap allocation this->reusable_iovs_.clear();
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs; this->reusable_iovs_.reserve(packets.size());
uint16_t total_write_len = 0; uint16_t total_write_len = 0;
for (const auto &msg : messages) { for (const auto &packet : packets) {
// Calculate varint sizes for header layout // Calculate varint sizes for header layout
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.payload_size)); uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.payload_size));
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.message_type)); uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.message_type));
uint8_t total_header_len = 1 + size_varint_len + type_varint_len; uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// Calculate where to start writing the header // Calculate where to start writing the header
@@ -283,25 +268,25 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
// //
// The message starts at offset + frame_header_padding_ // The message starts at offset + frame_header_padding_
// So we write the header starting at offset + frame_header_padding_ - total_header_len // So we write the header starting at offset + frame_header_padding_ - total_header_len
uint8_t *buf_start = buffer_data + msg.offset; uint8_t *buf_start = buffer_data + packet.offset;
uint32_t header_offset = frame_header_padding_ - total_header_len; uint32_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header // Write the plaintext header
buf_start[header_offset] = 0x00; // indicator buf_start[header_offset] = 0x00; // indicator
// Encode varints directly into buffer // Encode varints directly into buffer
ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
ProtoVarInt(msg.message_type) ProtoVarInt(packet.message_type)
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
// Add iovec for this message (header + payload) // Add iovec for this packet (header + payload)
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size); size_t packet_len = static_cast<size_t>(total_header_len + packet.payload_size);
iovs.push_back({buf_start + header_offset, msg_len}); this->reusable_iovs_.push_back({buf_start + header_offset, packet_len});
total_write_len += msg_len; total_write_len += packet_len;
} }
// Send all messages in one writev call // Send all packets in one writev call
return write_raw_(iovs.data(), iovs.size(), total_write_len); return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
} }
} // namespace esphome::api } // namespace esphome::api

View File

@@ -7,7 +7,8 @@ namespace esphome::api {
class APIPlaintextFrameHelper final : public APIFrameHelper { class APIPlaintextFrameHelper final : public APIFrameHelper {
public: public:
explicit APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) { APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info) {
// Plaintext header structure (worst case): // Plaintext header structure (worst case):
// Pos 0: indicator (0x00) // Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes) // Pos 1-3: payload size varint (up to 3 bytes)
@@ -20,7 +21,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
APIError loop() override; APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override; APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override; APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
protected: protected:
APIError try_read_frame_(); APIError try_read_frame_();

View File

@@ -27,6 +27,7 @@ extend google.protobuf.MessageOptions {
extend google.protobuf.FieldOptions { extend google.protobuf.FieldOptions {
optional string field_ifdef = 1042; optional string field_ifdef = 1042;
optional uint32 fixed_array_size = 50007; optional uint32 fixed_array_size = 50007;
optional bool no_zero_copy = 50008 [default=false];
optional bool fixed_array_skip_zero = 50009 [default=false]; optional bool fixed_array_skip_zero = 50009 [default=false];
optional string fixed_array_size_define = 50010; optional string fixed_array_size_define = 50010;
optional string fixed_array_with_length_define = 50011; optional string fixed_array_with_length_define = 50011;
@@ -79,15 +80,4 @@ extend google.protobuf.FieldOptions {
// Example: [(container_pointer_no_template) = "light::ColorModeMask"] // Example: [(container_pointer_no_template) = "light::ColorModeMask"]
// generates: const light::ColorModeMask *supported_color_modes{}; // generates: const light::ColorModeMask *supported_color_modes{};
optional string container_pointer_no_template = 50014; optional string container_pointer_no_template = 50014;
// packed_buffer: Expose raw packed buffer instead of decoding into container
// When set on a packed repeated field, the generated code stores a pointer
// to the raw protobuf buffer instead of decoding values. This enables
// zero-copy passthrough when the consumer can decode on-demand.
// The field must be a packed repeated field (packed=true).
// Generates three fields:
// - const uint8_t *<field>_data_{nullptr};
// - uint16_t <field>_length_{0};
// - uint16_t <field>_count_{0};
optional bool packed_buffer = 50015 [default=false];
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,6 @@
#include "esphome/components/climate/climate_traits.h" #include "esphome/components/climate/climate_traits.h"
#endif #endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
#include "esphome/components/light/light_traits.h" #include "esphome/components/light/light_traits.h"
#endif #endif

View File

@@ -13,7 +13,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str
} }
#endif #endif
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
switch (msg_type) { switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: { case HelloRequest::MESSAGE_TYPE: {
HelloRequest msg; HelloRequest msg;
@@ -24,6 +24,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_hello_request(msg); this->on_hello_request(msg);
break; break;
} }
#ifdef USE_API_PASSWORD
case AuthenticationRequest::MESSAGE_TYPE: {
AuthenticationRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_authentication_request: %s", msg.dump().c_str());
#endif
this->on_authentication_request(msg);
break;
}
#endif
case DisconnectRequest::MESSAGE_TYPE: { case DisconnectRequest::MESSAGE_TYPE: {
DisconnectRequest msg; DisconnectRequest msg;
// Empty message: no decode needed // Empty message: no decode needed
@@ -182,7 +193,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break; break;
} }
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
case ExecuteServiceRequest::MESSAGE_TYPE: { case ExecuteServiceRequest::MESSAGE_TYPE: {
ExecuteServiceRequest msg; ExecuteServiceRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
@@ -610,28 +621,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_homeassistant_action_response(msg); this->on_homeassistant_action_response(msg);
break; break;
} }
#endif
#ifdef USE_WATER_HEATER
case WaterHeaterCommandRequest::MESSAGE_TYPE: {
WaterHeaterCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_water_heater_command_request: %s", msg.dump().c_str());
#endif
this->on_water_heater_command_request(msg);
break;
}
#endif
#ifdef USE_IR_RF
case InfraredRFTransmitRawTimingsRequest::MESSAGE_TYPE: {
InfraredRFTransmitRawTimingsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_infrared_rf_transmit_raw_timings_request: %s", msg.dump().c_str());
#endif
this->on_infrared_rf_transmit_raw_timings_request(msg);
break;
}
#endif #endif
default: default:
break; break;
@@ -643,6 +632,13 @@ void APIServerConnection::on_hello_request(const HelloRequest &msg) {
this->on_fatal_error(); this->on_fatal_error();
} }
} }
#ifdef USE_API_PASSWORD
void APIServerConnection::on_authentication_request(const AuthenticationRequest &msg) {
if (!this->send_authenticate_response(msg)) {
this->on_fatal_error();
}
}
#endif
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
if (!this->send_disconnect_response(msg)) { if (!this->send_disconnect_response(msg)) {
this->on_fatal_error(); this->on_fatal_error();
@@ -674,7 +670,7 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc
this->subscribe_home_assistant_states(msg); this->subscribe_home_assistant_states(msg);
} }
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); } void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); }
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@@ -830,16 +826,14 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); } void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
#endif #endif
#ifdef USE_IR_RF
void APIServerConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
this->infrared_rf_transmit_raw_timings(msg);
}
#endif
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
// Check authentication/connection requirements for messages // Check authentication/connection requirements for messages
switch (msg_type) { switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required case HelloRequest::MESSAGE_TYPE: // No setup required
#ifdef USE_API_PASSWORD
case AuthenticationRequest::MESSAGE_TYPE: // No setup required
#endif
case DisconnectRequest::MESSAGE_TYPE: // No setup required case DisconnectRequest::MESSAGE_TYPE: // No setup required
case PingRequest::MESSAGE_TYPE: // No setup required case PingRequest::MESSAGE_TYPE: // No setup required
break; // Skip all checks for these messages break; // Skip all checks for these messages

View File

@@ -26,6 +26,10 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_hello_request(const HelloRequest &value){}; virtual void on_hello_request(const HelloRequest &value){};
#ifdef USE_API_PASSWORD
virtual void on_authentication_request(const AuthenticationRequest &value){};
#endif
virtual void on_disconnect_request(const DisconnectRequest &value){}; virtual void on_disconnect_request(const DisconnectRequest &value){};
virtual void on_disconnect_response(const DisconnectResponse &value){}; virtual void on_disconnect_response(const DisconnectResponse &value){};
virtual void on_ping_request(const PingRequest &value){}; virtual void on_ping_request(const PingRequest &value){};
@@ -75,7 +79,7 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_get_time_response(const GetTimeResponse &value){}; virtual void on_get_time_response(const GetTimeResponse &value){};
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
#endif #endif
@@ -87,10 +91,6 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_climate_command_request(const ClimateCommandRequest &value){}; virtual void on_climate_command_request(const ClimateCommandRequest &value){};
#endif #endif
#ifdef USE_WATER_HEATER
virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
#endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
virtual void on_number_command_request(const NumberCommandRequest &value){}; virtual void on_number_command_request(const NumberCommandRequest &value){};
#endif #endif
@@ -217,18 +217,16 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
#endif #endif
#ifdef USE_IR_RF
virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
#endif
protected: protected:
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
}; };
class APIServerConnection : public APIServerConnectionBase { class APIServerConnection : public APIServerConnectionBase {
public: public:
virtual bool send_hello_response(const HelloRequest &msg) = 0; virtual bool send_hello_response(const HelloRequest &msg) = 0;
#ifdef USE_API_PASSWORD
virtual bool send_authenticate_response(const AuthenticationRequest &msg) = 0;
#endif
virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0; virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0;
virtual bool send_ping_response(const PingRequest &msg) = 0; virtual bool send_ping_response(const PingRequest &msg) = 0;
virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0; virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0;
@@ -241,7 +239,7 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
virtual void execute_service(const ExecuteServiceRequest &msg) = 0; virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@@ -352,12 +350,12 @@ class APIServerConnection : public APIServerConnectionBase {
#endif #endif
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0; virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0;
#endif
#ifdef USE_IR_RF
virtual void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) = 0;
#endif #endif
protected: protected:
void on_hello_request(const HelloRequest &msg) override; void on_hello_request(const HelloRequest &msg) override;
#ifdef USE_API_PASSWORD
void on_authentication_request(const AuthenticationRequest &msg) override;
#endif
void on_disconnect_request(const DisconnectRequest &msg) override; void on_disconnect_request(const DisconnectRequest &msg) override;
void on_ping_request(const PingRequest &msg) override; void on_ping_request(const PingRequest &msg) override;
void on_device_info_request(const DeviceInfoRequest &msg) override; void on_device_info_request(const DeviceInfoRequest &msg) override;
@@ -370,7 +368,7 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
void on_execute_service_request(const ExecuteServiceRequest &msg) override; void on_execute_service_request(const ExecuteServiceRequest &msg) override;
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@@ -482,10 +480,7 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
#endif #endif
#ifdef USE_IR_RF void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
#endif
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -4,7 +4,6 @@
#include "api_connection.h" #include "api_connection.h"
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -35,7 +34,7 @@ APIServer::APIServer() {
} }
void APIServer::setup() { void APIServer::setup() {
ControllerRegistry::register_controller(this); this->setup_controller();
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
uint32_t hash = 88491486UL; uint32_t hash = 88491486UL;
@@ -52,6 +51,11 @@ void APIServer::setup() {
#endif #endif
#endif #endif
// Schedule reboot if no clients connect within timeout
if (this->reboot_timeout_ != 0) {
this->schedule_reboot_timeout_();
}
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->socket_ == nullptr) { if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket"); ESP_LOGW(TAG, "Could not create socket");
@@ -96,22 +100,42 @@ void APIServer::setup() {
#ifdef USE_LOGGER #ifdef USE_LOGGER
if (logger::global_logger != nullptr) { if (logger::global_logger != nullptr) {
logger::global_logger->add_log_listener(this); logger::global_logger->add_on_log_callback(
[this](int level, const char *tag, const char *message, size_t message_len) {
if (this->shutting_down_) {
// Don't try to send logs during shutdown
// as it could result in a recursion and
// we would be filling a buffer we are trying to clear
return;
}
for (auto &c : this->clients_) {
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
c->try_send_log_message(level, tag, message, message_len);
}
});
} }
#endif #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) { if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
camera::Camera::instance()->add_listener(this); camera::Camera::instance()->add_image_callback([this](const std::shared_ptr<camera::CameraImage> &image) {
for (auto &c : this->clients_) {
if (!c->flags_.remove)
c->set_camera_state(image);
}
});
} }
#endif #endif
}
// Initialize last_connected_ for reboot timeout tracking void APIServer::schedule_reboot_timeout_() {
this->last_connected_ = App.get_loop_component_start_time(); this->status_set_warning();
// Set warning status if reboot timeout is enabled this->set_timeout("api_reboot", this->reboot_timeout_, []() {
if (this->reboot_timeout_ != 0) { if (!global_api_server->is_connected()) {
this->status_set_warning(); ESP_LOGE(TAG, "No clients; rebooting");
} App.reboot();
}
});
} }
void APIServer::loop() { void APIServer::loop() {
@@ -125,41 +149,29 @@ void APIServer::loop() {
if (!sock) if (!sock)
break; break;
char peername[socket::SOCKADDR_STR_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit // Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) { if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, sock->getpeername().c_str());
// Immediately close - socket destructor will handle cleanup // Immediately close - socket destructor will handle cleanup
sock.reset(); sock.reset();
continue; continue;
} }
ESP_LOGD(TAG, "Accept %s", peername); ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this); auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn); this->clients_.emplace_back(conn);
conn->start(); conn->start();
// First client connected - clear warning and update timestamp // Clear warning status and cancel reboot when first client connects
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning(); this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time(); this->cancel_timeout("api_reboot");
} }
} }
} }
if (this->clients_.empty()) { if (this->clients_.empty()) {
// Check reboot timeout - done in loop to avoid scheduler heap churn
// (cancelled scheduler items sit in heap memory until their scheduled time)
if (this->reboot_timeout_ != 0) {
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_connected_ > this->reboot_timeout_) {
ESP_LOGE(TAG, "No clients; rebooting");
App.reboot();
}
}
return; return;
} }
@@ -169,7 +181,8 @@ void APIServer::loop() {
// Network is down - disconnect all clients // Network is down - disconnect all clients
for (auto &client : this->clients_) { for (auto &client : this->clients_) {
client->on_fatal_error(); client->on_fatal_error();
client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect")); ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(),
client->client_info_.peername.c_str());
} }
// Continue to process and clean up the clients below // Continue to process and clean up the clients below
} }
@@ -186,16 +199,10 @@ void APIServer::loop() {
} }
// Rare case: handle disconnection // Rare case: handle disconnection
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before removal for the trigger this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
std::string client_name(client->get_name());
std::string client_peername(client->get_peername());
#endif #endif
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
// Swap with the last element and pop (avoids expensive vector shifts) // Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) { if (client_index < this->clients_.size() - 1) {
@@ -203,16 +210,10 @@ void APIServer::loop() {
} }
this->clients_.pop_back(); this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout // Schedule reboot when last client disconnects
if (this->clients_.empty() && this->reboot_timeout_ != 0) { if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning(); this->schedule_reboot_timeout_();
this->last_connected_ = App.get_loop_component_start_time();
} }
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_->trigger(client_name, client_peername);
#endif
// Don't increment client_index since we need to process the swapped element // Don't increment client_index since we need to process the swapped element
} }
} }
@@ -223,10 +224,10 @@ void APIServer::dump_config() {
" Address: %s:%u\n" " Address: %s:%u\n"
" Listen backlog: %u\n" " Listen backlog: %u\n"
" Max connections: %u", " Max connections: %u",
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_); network::get_use_address().c_str(), this->port_, this->listen_backlog_, this->max_connections_);
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk())); ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
if (!this->noise_ctx_.has_psk()) { if (!this->noise_ctx_->has_psk()) {
ESP_LOGCONFIG(TAG, " Supports encryption: YES"); ESP_LOGCONFIG(TAG, " Supports encryption: YES");
} }
#else #else
@@ -234,9 +235,41 @@ void APIServer::dump_config() {
#endif #endif
} }
#ifdef USE_API_PASSWORD
bool APIServer::check_password(const uint8_t *password_data, size_t password_len) const {
// depend only on input password length
const char *a = this->password_.c_str();
uint32_t len_a = this->password_.length();
const char *b = reinterpret_cast<const char *>(password_data);
uint32_t len_b = password_len;
// disable optimization with volatile
volatile uint32_t length = len_b;
volatile const char *left = nullptr;
volatile const char *right = b;
uint8_t result = 0;
if (len_a == length) {
left = *((volatile const char **) &a);
result = 0;
}
if (len_a != length) {
left = b;
result = 1;
}
for (size_t i = 0; i < length; i++) {
result |= *left++ ^ *right++; // NOLINT
}
return result == 0;
}
#endif
void APIServer::handle_disconnect(APIConnection *conn) {} void APIServer::handle_disconnect(APIConnection *conn) {}
// Macro for controller update dispatch // Macro for entities without extra parameters
#define API_DISPATCH_UPDATE(entity_type, entity_name) \ #define API_DISPATCH_UPDATE(entity_type, entity_name) \
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \ if (obj->is_internal()) \
@@ -245,6 +278,15 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
c->send_##entity_name##_state(obj); \ c->send_##entity_name##_state(obj); \
} }
// Macro for entities with extra parameters (but parameters not used in send)
#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
#endif #endif
@@ -262,15 +304,15 @@ API_DISPATCH_UPDATE(light::LightState, light)
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
API_DISPATCH_UPDATE(sensor::Sensor, sensor) API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state)
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
API_DISPATCH_UPDATE(switch_::Switch, switch) API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state)
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
API_DISPATCH_UPDATE(text_sensor::TextSensor, text_sensor) API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state)
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
@@ -278,7 +320,7 @@ API_DISPATCH_UPDATE(climate::Climate, climate)
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
API_DISPATCH_UPDATE(number::Number, number) API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state)
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
@@ -294,11 +336,11 @@ API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
API_DISPATCH_UPDATE(text::Text, text) API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state)
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
API_DISPATCH_UPDATE(select::Select, select) API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index)
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
@@ -313,18 +355,13 @@ API_DISPATCH_UPDATE(valve::Valve, valve)
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif #endif
#ifdef USE_WATER_HEATER
API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
// Event is a special case - unlike other entities with simple state fields, // Event is a special case - it's the only entity that passes extra parameters to the send method
// events store their state in a member accessed via obj->get_last_event_type() void APIServer::on_event(event::Event *obj, const std::string &event_type) {
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal()) if (obj->is_internal())
return; return;
for (auto &c : this->clients_) for (auto &c : this->clients_)
c->send_event(obj, obj->get_last_event_type()); c->send_event(obj, event_type);
} }
#endif #endif
@@ -347,21 +384,6 @@ void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) {
} }
#endif #endif
#ifdef USE_IR_RF
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
const std::vector<int32_t> *timings) {
InfraredRFReceiveEvent resp{};
#ifdef USE_DEVICES
resp.device_id = device_id;
#endif
resp.key = key;
resp.timings = timings;
for (auto &c : this->clients_)
c->send_infrared_rf_receive_event(resp);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
#endif #endif
@@ -370,6 +392,10 @@ float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI;
void APIServer::set_port(uint16_t port) { this->port_ = port; } void APIServer::set_port(uint16_t port) { this->port_ = port; }
#ifdef USE_API_PASSWORD
void APIServer::set_password(const std::string &password) { this->password_ = password; }
#endif
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
@@ -383,7 +409,7 @@ void APIServer::register_action_response_callback(uint32_t call_id, ActionRespon
this->action_response_callbacks_.push_back({call_id, std::move(callback)}); this->action_response_callbacks_.push_back({call_id, std::move(callback)});
} }
void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message) { void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) {
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) { for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
if (it->call_id == call_id) { if (it->call_id == call_id) {
auto callback = std::move(it->callback); auto callback = std::move(it->callback);
@@ -395,7 +421,7 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef
} }
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message, void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
const uint8_t *response_data, size_t response_data_len) { const uint8_t *response_data, size_t response_data_len) {
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) { for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
if (it->call_id == call_id) { if (it->call_id == call_id) {
@@ -412,76 +438,25 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef
#endif // USE_API_HOMEASSISTANT_SERVICES #endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
// Helper to add subscription (reduces duplication) void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f, std::function<void(std::string)> f) {
bool once) {
this->state_subs_.push_back(HomeAssistantStateSubscription{ this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once, .entity_id = std::move(entity_id),
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation) .attribute = std::move(attribute),
.callback = std::move(f),
.once = false,
}); });
} }
// Helper to add subscription with heap-allocated strings (reduces duplication)
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f, bool once) {
HomeAssistantStateSubscription sub;
// Allocate heap storage for the strings
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
sub.entity_id = sub.entity_id_dynamic_storage->c_str();
if (attribute.has_value()) {
sub.attribute_dynamic_storage = std::make_unique<std::string>(std::move(attribute.value()));
sub.attribute = sub.attribute_dynamic_storage->c_str();
} else {
sub.attribute = nullptr;
}
sub.callback = std::move(f);
sub.once = once;
this->state_subs_.push_back(std::move(sub));
}
// New const char* overload (for internal components - zero allocation)
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(StringRef)> f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
}
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(StringRef)> f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
}
// std::string overload with StringRef callback (zero-allocation callback)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute, void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f) { std::function<void(std::string)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true); this->state_subs_.push_back(HomeAssistantStateSubscription{
} .entity_id = std::move(entity_id),
.attribute = std::move(attribute),
// Legacy helper: wraps std::string callback and delegates to StringRef version .callback = std::move(f),
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute, .once = true,
std::function<void(const std::string &)> f, bool once) { });
// Wrap callback to convert StringRef -> std::string, then delegate };
this->add_state_subscription_(std::move(entity_id), std::move(attribute),
std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
once);
}
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const { const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
return this->state_subs_; return this->state_subs_;
@@ -493,31 +468,6 @@ uint16_t APIServer::get_port() const { return this->port_; }
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) {
if (!this->noise_pref_.save(&new_psk)) {
ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
return false;
}
// ensure it's written immediately
if (!global_preferences->sync()) {
ESP_LOGW(TAG, "Failed to sync preferences");
return false;
}
ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
if (make_active) {
this->set_timeout(100, [this, active_psk]() {
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
this->set_noise_psk(active_psk);
for (auto &c : this->clients_) {
DisconnectRequest req;
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
}
});
}
return true;
}
bool APIServer::save_noise_psk(psk_t psk, bool make_active) { bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
#ifdef USE_API_NOISE_PSK_FROM_YAML #ifdef USE_API_NOISE_PSK_FROM_YAML
// When PSK is set from YAML, this function should never be called // When PSK is set from YAML, this function should never be called
@@ -525,28 +475,34 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
ESP_LOGW(TAG, "Key set in YAML"); ESP_LOGW(TAG, "Key set in YAML");
return false; return false;
#else #else
auto &old_psk = this->noise_ctx_.get_psk(); auto &old_psk = this->noise_ctx_->get_psk();
if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) { if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
ESP_LOGW(TAG, "New PSK matches old"); ESP_LOGW(TAG, "New PSK matches old");
return true; return true;
} }
SavedNoisePsk new_saved_psk{psk}; SavedNoisePsk new_saved_psk{psk};
return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk, if (!this->noise_pref_.save(&new_saved_psk)) {
make_active); ESP_LOGW(TAG, "Failed to save Noise PSK");
#endif return false;
} }
bool APIServer::clear_noise_psk(bool make_active) { // ensure it's written immediately
#ifdef USE_API_NOISE_PSK_FROM_YAML if (!global_preferences->sync()) {
// When PSK is set from YAML, this function should never be called ESP_LOGW(TAG, "Failed to sync preferences");
// but if it is, reject the change return false;
ESP_LOGW(TAG, "Key set in YAML"); }
return false; ESP_LOGD(TAG, "Noise PSK saved");
#else if (make_active) {
SavedNoisePsk empty_psk{}; this->set_timeout(100, [this, psk]() {
psk_t empty{}; ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty, this->set_noise_psk(psk);
make_active); for (auto &c : this->clients_) {
DisconnectRequest req;
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
}
});
}
return true;
#endif #endif
} }
#endif #endif
@@ -560,42 +516,7 @@ void APIServer::request_time() {
} }
#endif #endif
bool APIServer::is_connected(bool state_subscription_only) const { bool APIServer::is_connected() const { return !this->clients_.empty(); }
if (!state_subscription_only) {
return !this->clients_.empty();
}
for (const auto &client : this->clients_) {
if (client->flags_.state_subscription) {
return true;
}
}
return false;
}
#ifdef USE_LOGGER
void APIServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
if (this->shutting_down_) {
// Don't try to send logs during shutdown
// as it could result in a recursion and
// we would be filling a buffer we are trying to clear
return;
}
for (auto &c : this->clients_) {
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
c->try_send_log_message(level, tag, message, message_len);
}
}
#endif
#ifdef USE_CAMERA
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
for (auto &c : this->clients_) {
if (!c->flags_.remove)
c->set_camera_state(image);
}
}
#endif
void APIServer::on_shutdown() { void APIServer::on_shutdown() {
this->shutting_down_ = true; this->shutting_down_ = true;
@@ -632,91 +553,5 @@ bool APIServer::teardown() {
return this->clients_.empty(); return this->clients_.empty();
} }
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Timeout for action calls - matches aioesphomeapi client timeout (default 30s)
// Can be overridden via USE_API_ACTION_CALL_TIMEOUT_MS define for testing
#ifndef USE_API_ACTION_CALL_TIMEOUT_MS
#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT
#endif
// SSO-friendly action call key - hex format guarantees max 11 chars ("ac_ffffffff")
// which fits in any std::string SSO buffer (typically 12-15 bytes)
static inline std::string make_action_call_key(uint32_t id) {
char buf[12];
size_t len = snprintf(buf, sizeof(buf), "ac_%x", id);
return std::string(buf, len);
}
uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConnection *conn) {
uint32_t action_call_id = this->next_action_call_id_++;
// Handle wraparound (skip 0 as it means "no call")
if (this->next_action_call_id_ == 0) {
this->next_action_call_id_ = 1;
}
this->active_action_calls_.push_back({action_call_id, client_call_id, conn});
// Schedule automatic cleanup after timeout (client will have given up by then)
this->set_timeout(make_action_call_key(action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS, [this, action_call_id]() {
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
this->unregister_active_action_call(action_call_id);
});
return action_call_id;
}
void APIServer::unregister_active_action_call(uint32_t action_call_id) {
// Cancel the timeout for this action call
this->cancel_timeout(make_action_call_key(action_call_id));
// Swap-and-pop is more efficient than remove_if for unordered vectors
for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
if (this->active_action_calls_[i].action_call_id == action_call_id) {
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
this->active_action_calls_.pop_back();
return;
}
}
}
void APIServer::unregister_active_action_calls_for_connection(APIConnection *conn) {
// Remove all active action calls for disconnected connection using swap-and-pop
for (size_t i = 0; i < this->active_action_calls_.size();) {
if (this->active_action_calls_[i].connection == conn) {
// Cancel the timeout for this action call
this->cancel_timeout(make_action_call_key(this->active_action_calls_[i].action_call_id));
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
this->active_action_calls_.pop_back();
// Don't increment i - need to check the swapped element
} else {
i++;
}
}
}
void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message) {
for (auto &call : this->active_action_calls_) {
if (call.action_call_id == action_call_id) {
call.connection->send_execute_service_response(call.client_call_id, success, error_message);
return;
}
}
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message,
const uint8_t *response_data, size_t response_data_len) {
for (auto &call : this->active_action_calls_) {
if (call.action_call_id == action_call_id) {
call.connection->send_execute_service_response(call.client_call_id, success, error_message, response_data,
response_data_len);
return;
}
}
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
}
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
} // namespace esphome::api } // namespace esphome::api
#endif #endif

View File

@@ -10,42 +10,24 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/controller.h" #include "esphome/core/controller.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
#include "list_entities.h" #include "list_entities.h"
#include "subscribe_state.h" #include "subscribe_state.h"
#ifdef USE_LOGGER #ifdef USE_API_SERVICES
#include "esphome/components/logger/logger.h" #include "user_services.h"
#endif
#ifdef USE_CAMERA
#include "esphome/components/camera/camera.h"
#endif #endif
#include <map>
#include <vector> #include <vector>
namespace esphome::api { namespace esphome::api {
#ifdef USE_API_USER_DEFINED_ACTIONS
// Forward declaration - full definition in user_services.h
class UserServiceDescriptor;
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
struct SavedNoisePsk { struct SavedNoisePsk {
psk_t psk; psk_t psk;
} PACKED; // NOLINT } PACKED; // NOLINT
#endif #endif
class APIServer : public Component, class APIServer : public Component, public Controller {
public Controller
#ifdef USE_LOGGER
,
public logger::LogListener
#endif
#ifdef USE_CAMERA
,
public camera::CameraListener
#endif
{
public: public:
APIServer(); APIServer();
void setup() override; void setup() override;
@@ -55,11 +37,9 @@ class APIServer : public Component,
void dump_config() override; void dump_config() override;
void on_shutdown() override; void on_shutdown() override;
bool teardown() override; bool teardown() override;
#ifdef USE_LOGGER #ifdef USE_API_PASSWORD
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; bool check_password(const uint8_t *password_data, size_t password_len) const;
#endif void set_password(const std::string &password);
#ifdef USE_CAMERA
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
#endif #endif
void set_port(uint16_t port); void set_port(uint16_t port);
void set_reboot_timeout(uint32_t reboot_timeout); void set_reboot_timeout(uint32_t reboot_timeout);
@@ -73,9 +53,8 @@ class APIServer : public Component,
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true); bool save_noise_psk(psk_t psk, bool make_active = true);
bool clear_noise_psk(bool make_active = true); void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); } std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
#endif // USE_API_NOISE #endif // USE_API_NOISE
void handle_disconnect(APIConnection *conn); void handle_disconnect(APIConnection *conn);
@@ -92,19 +71,19 @@ class APIServer : public Component,
void on_light_update(light::LightState *obj) override; void on_light_update(light::LightState *obj) override;
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
void on_sensor_update(sensor::Sensor *obj) override; void on_sensor_update(sensor::Sensor *obj, float state) override;
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
void on_switch_update(switch_::Switch *obj) override; void on_switch_update(switch_::Switch *obj, bool state) override;
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
void on_text_sensor_update(text_sensor::TextSensor *obj) override; void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override;
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
void on_climate_update(climate::Climate *obj) override; void on_climate_update(climate::Climate *obj) override;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
void on_number_update(number::Number *obj) override; void on_number_update(number::Number *obj, float state) override;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
void on_date_update(datetime::DateEntity *obj) override; void on_date_update(datetime::DateEntity *obj) override;
@@ -116,10 +95,10 @@ class APIServer : public Component,
void on_datetime_update(datetime::DateTimeEntity *obj) override; void on_datetime_update(datetime::DateTimeEntity *obj) override;
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
void on_text_update(text::Text *obj) override; void on_text_update(text::Text *obj, const std::string &state) override;
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
void on_select_update(select::Select *obj) override; void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override; void on_lock_update(lock::Lock *obj) override;
@@ -130,9 +109,6 @@ class APIServer : public Component,
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override; void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif #endif
#ifdef USE_WATER_HEATER
void on_water_heater_update(water_heater::WaterHeater *obj) override;
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call); void send_homeassistant_action(const HomeassistantActionRequest &call);
@@ -140,35 +116,16 @@ class APIServer : public Component,
// Action response handling // Action response handling
using ActionResponseCallback = std::function<void(const class ActionResponse &)>; using ActionResponseCallback = std::function<void(const class ActionResponse &)>;
void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback); void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback);
void handle_action_response(uint32_t call_id, bool success, StringRef error_message); void handle_action_response(uint32_t call_id, bool success, const std::string &error_message);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
void handle_action_response(uint32_t call_id, bool success, StringRef error_message, const uint8_t *response_data, void handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
size_t response_data_len); const uint8_t *response_data, size_t response_data_len);
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES #endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
void initialize_user_services(std::initializer_list<UserServiceDescriptor *> services) {
this->user_services_.assign(services);
}
#ifdef USE_API_CUSTOM_SERVICES
// Only compile push_back method when custom_services: true (external components)
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Action call context management - supports concurrent calls from multiple clients
// Returns server-generated action_call_id to avoid collisions when clients use same call_id
uint32_t register_active_action_call(uint32_t client_call_id, APIConnection *conn);
void unregister_active_action_call(uint32_t action_call_id);
void unregister_active_action_calls_for_connection(APIConnection *conn);
// Send response for a specific action call (uses action_call_id, sends client_call_id in response)
void send_action_response(uint32_t action_call_id, bool success, StringRef error_message);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void send_action_response(uint32_t action_call_id, bool success, StringRef error_message,
const uint8_t *response_data, size_t response_data_len);
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void request_time(); void request_time();
#endif #endif
@@ -177,7 +134,7 @@ class APIServer : public Component,
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override;
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
void on_event(event::Event *obj) override; void on_event(event::Event *obj, const std::string &event_type) override;
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
void on_update(update::UpdateEntity *obj) override; void on_update(update::UpdateEntity *obj) override;
@@ -185,44 +142,24 @@ class APIServer : public Component,
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
#endif #endif
#ifdef USE_IR_RF
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
#endif
bool is_connected(bool state_subscription_only = false) const; bool is_connected() const;
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription { struct HomeAssistantStateSubscription {
const char *entity_id; // Pointer to flash (internal) or heap (external) std::string entity_id;
const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute) optional<std::string> attribute;
std::function<void(StringRef)> callback; std::function<void(std::string)> callback;
bool once; bool once;
// Dynamic storage for external components using std::string API (custom_api_device.h)
// These are only allocated when using the std::string overload (nullptr for const char* overload)
std::unique_ptr<std::string> entity_id_dynamic_storage;
std::unique_ptr<std::string> attribute_dynamic_storage;
}; };
// New const char* overload (for internal components - zero allocation)
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
// std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f); std::function<void(std::string)> f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute, void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f); std::function<void(std::string)> f);
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; } const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
#endif #endif
@@ -236,20 +173,7 @@ class APIServer : public Component,
#endif #endif
protected: protected:
#ifdef USE_API_NOISE void schedule_reboot_timeout_();
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
const psk_t &active_psk, bool make_active);
#endif // USE_API_NOISE
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper methods to reduce code duplication
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute, std::function<void(StringRef)> f,
bool once);
// Legacy helper: wraps std::string callback and delegates to StringRef version
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once);
#endif // USE_API_HOMEASSISTANT_STATES
// Pointers and pointer-like types first (4 bytes each) // Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr; std::unique_ptr<socket::Socket> socket_ = nullptr;
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER #ifdef USE_API_CLIENT_CONNECTED_TRIGGER
@@ -261,27 +185,18 @@ class APIServer : public Component,
// 4-byte aligned types // 4-byte aligned types
uint32_t reboot_timeout_{300000}; uint32_t reboot_timeout_{300000};
uint32_t last_connected_{0};
// Vectors and strings (12 bytes each on 32-bit) // Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_; std::vector<std::unique_ptr<APIConnection>> clients_;
#ifdef USE_API_PASSWORD
std::string password_;
#endif
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
std::vector<HomeAssistantStateSubscription> state_subs_; std::vector<HomeAssistantStateSubscription> state_subs_;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
std::vector<UserServiceDescriptor *> user_services_; std::vector<UserServiceDescriptor *> user_services_;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Active action calls - supports concurrent calls from multiple clients
// Uses server-generated action_call_id to avoid collisions when multiple clients use same call_id
struct ActiveActionCall {
uint32_t action_call_id; // Server-generated unique ID (passed to actions)
uint32_t client_call_id; // Client's original call_id (used in response)
APIConnection *connection;
};
std::vector<ActiveActionCall> active_action_calls_;
uint32_t next_action_call_id_{1}; // Counter for generating unique action_call_ids
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
struct PendingActionResponse { struct PendingActionResponse {
@@ -302,7 +217,7 @@ class APIServer : public Component,
// 7 bytes used, 1 byte padding // 7 bytes used, 1 byte padding
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
APINoiseContext noise_ctx_; std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
ESPPreferenceObject noise_pref_; ESPPreferenceObject noise_pref_;
#endif // USE_API_NOISE #endif // USE_API_NOISE
}; };
@@ -310,11 +225,8 @@ class APIServer : public Component,
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> { template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
TEMPLATABLE_VALUE(bool, state_subscription_only)
public: public:
bool check(const Ts &...x) override { bool check(Ts... x) override { return global_api_server->is_connected(); }
return global_api_server->is_connected(this->state_subscription_only_.value(x...));
}
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -16,7 +16,7 @@ with warnings.catch_warnings():
import contextlib import contextlib
from esphome.const import CONF_KEY, CONF_PORT, __version__ from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
from esphome.core import CORE from esphome.core import CORE
from . import CONF_ENCRYPTION from . import CONF_ENCRYPTION
@@ -35,6 +35,7 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
conf = config["api"] conf = config["api"]
name = config["esphome"]["name"] name = config["esphome"]["name"]
port: int = int(conf[CONF_PORT]) port: int = int(conf[CONF_PORT])
password: str = conf[CONF_PASSWORD]
noise_psk: str | None = None noise_psk: str | None = None
if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)): if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)):
noise_psk = key noise_psk = key
@@ -49,7 +50,7 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
cli = APIClient( cli = APIClient(
addresses[0], # Primary address for compatibility addresses[0], # Primary address for compatibility
port, port,
"", # Password auth removed in 2026.1.0 password,
client_info=f"ESPHome Logs {__version__}", client_info=f"ESPHome Logs {__version__}",
noise_psk=noise_psk, noise_psk=noise_psk,
addresses=addresses, # Pass all addresses for automatic retry addresses=addresses, # Pass all addresses for automatic retry

View File

@@ -3,28 +3,25 @@
#include <map> #include <map>
#include "api_server.h" #include "api_server.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
#include "user_services.h" #include "user_services.h"
#endif #endif
namespace esphome::api { namespace esphome::api {
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceDynamic<Ts...> { template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
public: public:
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj, CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
void (T::*callback)(Ts...)) void (T::*callback)(Ts...))
: UserServiceDynamic<Ts...>(name, arg_names), obj_(obj), callback_(callback) {} : UserServiceBase<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
protected: protected:
// CustomAPIDevice services don't support action responses - ignore call_id and return_response void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override {
(this->obj_->*this->callback_)(x...); // NOLINT
}
T *obj_; T *obj_;
void (T::*callback_)(Ts...); void (T::*callback_)(Ts...);
}; };
#endif // USE_API_USER_DEFINED_ACTIONS #endif // USE_API_SERVICES
class CustomAPIDevice { class CustomAPIDevice {
public: public:
@@ -52,18 +49,12 @@ class CustomAPIDevice {
* @param name The name of the service to register. * @param name The name of the service to register.
* @param arg_names The name of the arguments for the service, must match the arguments of the function. * @param arg_names The name of the arguments for the service, must match the arguments of the function.
*/ */
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
template<typename T, typename... Ts> template<typename T, typename... Ts>
void register_service(void (T::*callback)(Ts...), const std::string &name, void register_service(void (T::*callback)(Ts...), const std::string &name,
const std::array<std::string, sizeof...(Ts)> &arg_names) { const std::array<std::string, sizeof...(Ts)> &arg_names) {
#ifdef USE_API_CUSTOM_SERVICES
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
#else
static_assert(
sizeof(T) == 0,
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
#endif
} }
#else #else
template<typename T, typename... Ts> template<typename T, typename... Ts>
@@ -93,16 +84,10 @@ class CustomAPIDevice {
* @param callback The member function to call when the service is triggered. * @param callback The member function to call when the service is triggered.
* @param name The name of the arguments for the service, must match the arguments of the function. * @param name The name of the arguments for the service, must match the arguments of the function.
*/ */
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
template<typename T> void register_service(void (T::*callback)(), const std::string &name) { template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
#ifdef USE_API_CUSTOM_SERVICES
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
#else
static_assert(
sizeof(T) == 0,
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
#endif
} }
#else #else
template<typename T> void register_service(void (T::*callback)(), const std::string &name) { template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
@@ -122,36 +107,21 @@ class CustomAPIDevice {
* subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature"); * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature");
* } * }
* *
* void on_state_changed(StringRef state) { * void on_state_changed(std::string state) {
* // State of climate.kitchen current_temperature is `state` * // State of sensor.weather_forecast is `state`
* // Use state.c_str() for C string, state.str() for std::string
* } * }
* ``` * ```
* *
* @tparam T The class type creating the service, automatically deduced from the function pointer. * @tparam T The class type creating the service, automatically deduced from the function pointer.
* @param callback The member function to call when the entity state changes (zero-allocation). * @param callback The member function to call when the entity state changes.
* @param entity_id The entity_id to track. * @param entity_id The entity_id to track.
* @param attribute The entity state attribute to track. * @param attribute The entity state attribute to track.
*/ */
template<typename T> template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
*
* @deprecated Use the StringRef overload for zero-allocation callbacks. Will be removed in 2027.1.0.
*/
template<typename T>
ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") { const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1); auto f = std::bind(callback, (T *) this, std::placeholders::_1);
// Explicit type to disambiguate overload resolution global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(f));
} }
/** Subscribe to the state (or attribute state) of an entity from Home Assistant. /** Subscribe to the state (or attribute state) of an entity from Home Assistant.
@@ -163,45 +133,23 @@ class CustomAPIDevice {
* subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast"); * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
* } * }
* *
* void on_state_changed(const std::string &entity_id, StringRef state) { * void on_state_changed(std::string entity_id, std::string state) {
* // State of `entity_id` is `state` * // State of `entity_id` is `state`
* } * }
* ``` * ```
* *
* @tparam T The class type creating the service, automatically deduced from the function pointer. * @tparam T The class type creating the service, automatically deduced from the function pointer.
* @param callback The member function to call when the entity state changes (zero-allocation for state). * @param callback The member function to call when the entity state changes.
* @param entity_id The entity_id to track. * @param entity_id The entity_id to track.
* @param attribute The entity state attribute to track. * @param attribute The entity state attribute to track.
*/ */
template<typename T> template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
*
* @deprecated Use the StringRef overload for zero-allocation callbacks. Will be removed in 2027.1.0.
*/
template<typename T>
ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") { const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
// Explicit type to disambiguate overload resolution global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(f));
} }
#else #else
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");
}
template<typename T> template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") { const std::string &attribute = "") {
@@ -210,14 +158,6 @@ class CustomAPIDevice {
"of your YAML configuration"); "of your YAML configuration");
} }
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");
}
template<typename T> template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") { const std::string &attribute = "") {
@@ -240,7 +180,7 @@ class CustomAPIDevice {
*/ */
void call_homeassistant_service(const std::string &service_name) { void call_homeassistant_service(const std::string &service_name) {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
resp.service = StringRef(service_name); resp.set_service(StringRef(service_name));
global_api_server->send_homeassistant_action(resp); global_api_server->send_homeassistant_action(resp);
} }
@@ -260,12 +200,12 @@ class CustomAPIDevice {
*/ */
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) { void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
resp.service = StringRef(service_name); resp.set_service(StringRef(service_name));
resp.data.init(data.size()); resp.data.init(data.size());
for (auto &it : data) { for (auto &it : data) {
auto &kv = resp.data.emplace_back(); auto &kv = resp.data.emplace_back();
kv.key = StringRef(it.first); kv.set_key(StringRef(it.first));
kv.value = StringRef(it.second); // data map lives until send completes kv.value = it.second;
} }
global_api_server->send_homeassistant_action(resp); global_api_server->send_homeassistant_action(resp);
} }
@@ -282,7 +222,7 @@ class CustomAPIDevice {
*/ */
void fire_homeassistant_event(const std::string &event_name) { void fire_homeassistant_event(const std::string &event_name) {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
resp.service = StringRef(event_name); resp.set_service(StringRef(event_name));
resp.is_event = true; resp.is_event = true;
global_api_server->send_homeassistant_action(resp); global_api_server->send_homeassistant_action(resp);
} }
@@ -302,13 +242,13 @@ class CustomAPIDevice {
*/ */
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) { void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
resp.service = StringRef(service_name); resp.set_service(StringRef(service_name));
resp.is_event = true; resp.is_event = true;
resp.data.init(data.size()); resp.data.init(data.size());
for (auto &it : data) { for (auto &it : data) {
auto &kv = resp.data.emplace_back(); auto &kv = resp.data.emplace_back();
kv.key = StringRef(it.first); kv.set_key(StringRef(it.first));
kv.value = StringRef(it.second); // data map lives until send completes kv.value = it.second;
} }
global_api_server->send_homeassistant_action(resp); global_api_server->send_homeassistant_action(resp);
} }

View File

@@ -12,17 +12,10 @@
#endif #endif
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
namespace esphome::api { namespace esphome::api {
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> { template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
// Verify that const char* uses the base class STATIC_STRING optimization (no heap allocation)
// rather than being wrapped in a lambda. The base class constructor for const char* is more
// specialized than the templated constructor here, so it should be selected.
static_assert(std::is_constructible_v<TemplatableValue<std::string, X...>, const char *>,
"Base class must have const char* constructor for STATIC_STRING optimization");
private: private:
// Helper to convert value to string - handles the case where value is already a string // 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)); } template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
@@ -53,25 +46,23 @@ template<typename... Ts> class TemplatableKeyValuePair {
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event") // Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
// and never templatable values or lambdas. Only the value parameter can be a lambda/template. // and never templatable values or lambdas. Only the value parameter can be a lambda/template.
// Using const char* avoids std::string heap allocation - keys remain in flash. // Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
template<typename T> TemplatableKeyValuePair(const char *key, T value) : key(key), value(value) {} template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
const char *key{nullptr}; std::string key;
TemplatableStringValue<Ts...> value; TemplatableStringValue<Ts...> value;
}; };
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
// Represents the response data from a Home Assistant action // Represents the response data from a Home Assistant action
// Note: This class holds a StringRef to the error_message from the protobuf message.
// The protobuf message must outlive the ActionResponse (which is guaranteed since
// the callback is invoked synchronously while the message is on the stack).
class ActionResponse { class ActionResponse {
public: public:
ActionResponse(bool success, StringRef error_message) : success_(success), error_message_(error_message) {} ActionResponse(bool success, std::string error_message = "")
: success_(success), error_message_(std::move(error_message)) {}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
ActionResponse(bool success, StringRef error_message, const uint8_t *data, size_t data_len) ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
: success_(success), error_message_(error_message) { : success_(success), error_message_(std::move(error_message)) {
if (data == nullptr || data_len == 0) if (data == nullptr || data_len == 0)
return; return;
this->json_document_ = json::parse_json(data, data_len); this->json_document_ = json::parse_json(data, data_len);
@@ -79,8 +70,7 @@ class ActionResponse {
#endif #endif
bool is_success() const { return this->success_; } bool is_success() const { return this->success_; }
// Returns reference to error message - can be implicitly converted to std::string if needed const std::string &get_error_message() const { return this->error_message_; }
const StringRef &get_error_message() const { return this->error_message_; }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
// Get data as parsed JSON object (const version returns read-only view) // Get data as parsed JSON object (const version returns read-only view)
@@ -89,7 +79,7 @@ class ActionResponse {
protected: protected:
bool success_; bool success_;
StringRef error_message_; std::string error_message_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
JsonDocument json_document_; JsonDocument json_document_;
#endif #endif
@@ -115,15 +105,14 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
// Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))). // Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
// The value parameter can be a lambda/template, but keys are never templatable. // The value parameter can be a lambda/template, but keys are never templatable.
// Using const char* for keys avoids std::string heap allocation - keys remain in flash. template<typename K, typename V> void add_data(K &&key, V &&value) {
template<typename V> void add_data(const char *key, V &&value) { this->add_kv_(this->data_, std::forward<K>(key), std::forward<V>(value));
this->add_kv_(this->data_, key, std::forward<V>(value));
} }
template<typename V> void add_data_template(const char *key, V &&value) { template<typename K, typename V> void add_data_template(K &&key, V &&value) {
this->add_kv_(this->data_template_, key, std::forward<V>(value)); this->add_kv_(this->data_template_, std::forward<K>(key), std::forward<V>(value));
} }
template<typename V> void add_variable(const char *key, V &&value) { template<typename K, typename V> void add_variable(K &&key, V &&value) {
this->add_kv_(this->variables_, key, std::forward<V>(value)); this->add_kv_(this->variables_, std::forward<K>(key), std::forward<V>(value));
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -144,26 +133,16 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; } Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
void play(const Ts &...x) override { void play(Ts... x) override {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
std::string service_value = this->service_.value(x...); std::string service_value = this->service_.value(x...);
resp.service = StringRef(service_value); resp.set_service(StringRef(service_value));
resp.is_event = this->flags_.is_event; resp.is_event = this->flags_.is_event;
this->populate_service_map(resp.data, this->data_, x...);
// Local storage for lambda-evaluated strings - lives until after send this->populate_service_map(resp.data_template, this->data_template_, x...);
FixedVector<std::string> data_storage; this->populate_service_map(resp.variables, this->variables_, x...);
FixedVector<std::string> data_template_storage;
FixedVector<std::string> variables_storage;
this->populate_service_map(resp.data, this->data_, data_storage, x...);
this->populate_service_map(resp.data_template, this->data_template_, data_template_storage, x...);
this->populate_service_map(resp.variables, this->variables_, variables_storage, x...);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
// IMPORTANT: Declare at outer scope so it lives until send_homeassistant_action returns.
std::string response_template_value;
#endif
if (this->flags_.wants_status) { if (this->flags_.wants_status) {
// Generate a unique call ID for this service call // Generate a unique call ID for this service call
static uint32_t call_id_counter = 1; static uint32_t call_id_counter = 1;
@@ -174,8 +153,8 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
resp.wants_response = true; resp.wants_response = true;
// Set response template if provided // Set response template if provided
if (this->flags_.has_response_template) { if (this->flags_.has_response_template) {
response_template_value = this->response_template_.value(x...); std::string response_template_value = this->response_template_.value(x...);
resp.response_template = StringRef(response_template_value); resp.response_template = response_template_value;
} }
} }
#endif #endif
@@ -206,40 +185,20 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
} }
protected: protected:
// Helper to add key-value pairs to FixedVectors // Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies
// Keys are always string literals (const char*), values can be lambdas/templates template<typename K, typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, K &&key, V &&value) {
template<typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, const char *key, V &&value) {
auto &kv = vec.emplace_back(); auto &kv = vec.emplace_back();
kv.key = key; kv.key = std::forward<K>(key);
kv.value = std::forward<V>(value); kv.value = std::forward<V>(value);
} }
template<typename VectorType, typename SourceType> template<typename VectorType, typename SourceType>
static void populate_service_map(VectorType &dest, SourceType &source, FixedVector<std::string> &value_storage, static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) {
Ts... x) {
dest.init(source.size()); dest.init(source.size());
// Count non-static strings to allocate exact storage needed
size_t lambda_count = 0;
for (const auto &it : source) {
if (!it.value.is_static_string()) {
lambda_count++;
}
}
value_storage.init(lambda_count);
for (auto &it : source) { for (auto &it : source) {
auto &kv = dest.emplace_back(); auto &kv = dest.emplace_back();
kv.key = StringRef(it.key); kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
if (it.value.is_static_string()) {
// Static string from YAML - zero allocation
kv.value = StringRef(it.value.get_static_string());
} else {
// Lambda evaluation - store result, reference it
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
} }
} }

View File

@@ -5,9 +5,6 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
#ifdef USE_API_USER_DEFINED_ACTIONS
#include "user_services.h"
#endif
namespace esphome::api { namespace esphome::api {
@@ -73,12 +70,6 @@ LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMedia
LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel, LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel,
ListEntitiesAlarmControlPanelResponse) ListEntitiesAlarmControlPanelResponse)
#endif #endif
#ifdef USE_WATER_HEATER
LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse)
#endif
#ifdef USE_INFRARED
LIST_ENTITIES_HANDLER(infrared, infrared::Infrared, ListEntitiesInfraredResponse)
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse) LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
#endif #endif
@@ -91,7 +82,7 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response(); auto resp = service->encode_list_service_response();
return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE);

View File

@@ -43,7 +43,7 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *entity) override; bool on_text_sensor(text_sensor::TextSensor *entity) override;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
bool on_service(UserServiceDescriptor *service) override; bool on_service(UserServiceDescriptor *service) override;
#endif #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
@@ -82,12 +82,6 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif #endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_INFRARED
bool on_infrared(infrared::Infrared *entity) override;
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool on_event(event::Event *entity) override; bool on_event(event::Event *entity) override;
#endif #endif

View File

@@ -39,24 +39,6 @@ inline constexpr int64_t decode_zigzag64(uint64_t value) {
return (value & 1) ? static_cast<int64_t>(~(value >> 1)) : static_cast<int64_t>(value >> 1); return (value & 1) ? static_cast<int64_t>(~(value >> 1)) : static_cast<int64_t>(value >> 1);
} }
/// Count number of varints in a packed buffer
inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
uint16_t count = 0;
while (len > 0) {
// Skip varint bytes until we find one without continuation bit
while (len > 0 && (*data & 0x80)) {
data++;
len--;
}
if (len > 0) {
data++;
len--;
count++;
}
}
return count;
}
/* /*
* StringRef Ownership Model for API Protocol Messages * StringRef Ownership Model for API Protocol Messages
* =================================================== * ===================================================
@@ -72,16 +54,16 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
* 3. Global/static strings: StringRef(GLOBAL_CONSTANT) - Always safe * 3. Global/static strings: StringRef(GLOBAL_CONSTANT) - Always safe
* 4. Local variables: Safe ONLY if encoding happens before function returns: * 4. Local variables: Safe ONLY if encoding happens before function returns:
* std::string temp = compute_value(); * std::string temp = compute_value();
* msg.field = StringRef(temp); * msg.set_field(StringRef(temp));
* return this->send_message(msg); // temp is valid during encoding * return this->send_message(msg); // temp is valid during encoding
* *
* Unsafe Patterns (WILL cause crashes/corruption): * Unsafe Patterns (WILL cause crashes/corruption):
* 1. Temporaries: msg.field = StringRef(obj.get_string()) // get_string() returns by value * 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value
* 2. Concatenation: msg.field = StringRef(str1 + str2) // Result is temporary * 2. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
* *
* For unsafe patterns, store in a local variable first: * For unsafe patterns, store in a local variable first:
* std::string temp = get_string(); // or str1 + str2 * std::string temp = get_string(); // or str1 + str2
* msg.field = StringRef(temp); * msg.set_field(StringRef(temp));
* *
* The send_*_response pattern ensures proper lifetime management by encoding * The send_*_response pattern ensures proper lifetime management by encoding
* within the same function scope where temporaries are created. * within the same function scope where temporaries are created.
@@ -198,10 +180,9 @@ class ProtoVarInt {
uint64_t value_; uint64_t value_;
}; };
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32 // Forward declaration for decode_to_message and encode_to_writer
class ProtoDecodableMessage;
class ProtoMessage; class ProtoMessage;
class ProtoSize; class ProtoDecodableMessage;
class ProtoLengthDelimited { class ProtoLengthDelimited {
public: public:
@@ -353,15 +334,16 @@ class ProtoWriteBuffer {
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
this->encode_uint64(field_id, encode_zigzag64(value), force); this->encode_uint64(field_id, encode_zigzag64(value), force);
} }
/// Encode a packed repeated sint32 field (zero-copy from vector) void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false);
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
void encode_message(uint32_t field_id, const ProtoMessage &value);
std::vector<uint8_t> *get_buffer() const { return buffer_; } std::vector<uint8_t> *get_buffer() const { return buffer_; }
protected: protected:
std::vector<uint8_t> *buffer_; std::vector<uint8_t> *buffer_;
}; };
// Forward declaration
class ProtoSize;
class ProtoMessage { class ProtoMessage {
public: public:
virtual ~ProtoMessage() = default; virtual ~ProtoMessage() = default;
@@ -810,45 +792,10 @@ class ProtoSize {
} }
} }
} }
/**
* @brief Calculate size of a packed repeated sint32 field
*/
inline void add_packed_sint32(uint32_t field_id_size, const std::vector<int32_t> &values) {
if (values.empty())
return;
size_t packed_size = 0;
for (int value : values) {
packed_size += varint(encode_zigzag32(value));
}
// field_id + length varint + packed data
total_size_ += field_id_size + varint(static_cast<uint32_t>(packed_size)) + static_cast<uint32_t>(packed_size);
}
}; };
// Implementation of encode_packed_sint32 - must be after ProtoSize is defined
inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) {
if (values.empty())
return;
// Calculate packed size
size_t packed_size = 0;
for (int value : values) {
packed_size += ProtoSize::varint(encode_zigzag32(value));
}
// Write tag (LENGTH_DELIMITED) + length + all zigzag-encoded values
this->encode_field_raw(field_id, WIRE_TYPE_LENGTH_DELIMITED);
this->encode_varint_raw(packed_size);
for (int value : values) {
this->encode_varint_raw(encode_zigzag32(value));
}
}
// Implementation of encode_message - must be after ProtoMessage is defined // Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) { inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
// Calculate the message size first // Calculate the message size first
@@ -886,6 +833,9 @@ class ProtoService {
virtual bool is_authenticated() = 0; virtual bool is_authenticated() = 0;
virtual bool is_connection_setup() = 0; virtual bool is_connection_setup() = 0;
virtual void on_fatal_error() = 0; virtual void on_fatal_error() = 0;
#ifdef USE_API_PASSWORD
virtual void on_unauthenticated_access() = 0;
#endif
virtual void on_no_setup_connection() = 0; virtual void on_no_setup_connection() = 0;
/** /**
* Create a buffer with a reserved size. * Create a buffer with a reserved size.
@@ -896,7 +846,7 @@ class ProtoService {
*/ */
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
// Optimized method that pre-allocates buffer based on message size // Optimized method that pre-allocates buffer based on message size
bool send_message_(const ProtoMessage &msg, uint8_t message_type) { bool send_message_(const ProtoMessage &msg, uint8_t message_type) {
@@ -923,7 +873,20 @@ class ProtoService {
return true; return true;
} }
inline bool check_authenticated_() { return this->check_connection_setup_(); } inline bool check_authenticated_() {
#ifdef USE_API_PASSWORD
if (!this->check_connection_setup_()) {
return false;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return false;
}
return true;
#else
return this->check_connection_setup_();
#endif
}
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -60,9 +60,6 @@ INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer)
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel) INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel)
#endif #endif
#ifdef USE_WATER_HEATER
INITIAL_STATE_HANDLER(water_heater, water_heater::WaterHeater)
#endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
INITIAL_STATE_HANDLER(update, update::UpdateEntity) INITIAL_STATE_HANDLER(update, update::UpdateEntity)
#endif #endif

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