mirror of
https://github.com/esphome/esphome.git
synced 2025-11-10 03:51:52 +00:00
Compare commits
1 Commits
no_batch_e
...
idf_spi_es
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f195ac1afd |
@@ -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:**
|
||||||
@@ -258,11 +186,6 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
└── components/[component]/ # Component-specific tests
|
└── components/[component]/ # Component-specific tests
|
||||||
```
|
```
|
||||||
Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
|
Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
|
||||||
* **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
|
|
||||||
```bash
|
|
||||||
./script/test_component_grouping.py -e config --all
|
|
||||||
```
|
|
||||||
This tests all components in a single build to catch conflicts that might not appear when testing components individually. Use `-e config` for fast configuration validation, or `-e compile` for full compilation testing.
|
|
||||||
* **Debugging and Troubleshooting:**
|
* **Debugging and Troubleshooting:**
|
||||||
* **Debug Tools:**
|
* **Debug Tools:**
|
||||||
- `esphome config <file>.yaml` to validate configuration.
|
- `esphome config <file>.yaml` to validate configuration.
|
||||||
@@ -293,146 +216,6 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
* **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests.
|
* **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests.
|
||||||
* **Code Generation:** Generate minimal and efficient C++ code. Validate all user inputs thoroughly. Support multiple platform variations.
|
* **Code Generation:** Generate minimal and efficient C++ code. Validate all user inputs thoroughly. Support multiple platform variations.
|
||||||
* **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization.
|
* **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.
|
|
||||||
|
|
||||||
**STL Container Guidelines:**
|
|
||||||
|
|
||||||
ESPHome runs on embedded systems with limited resources. Choose containers carefully:
|
|
||||||
|
|
||||||
1. **Compile-time-known sizes:** Use `std::array` instead of `std::vector` when size is known at compile time.
|
|
||||||
```cpp
|
|
||||||
// Bad - generates STL realloc code
|
|
||||||
std::vector<int> values;
|
|
||||||
|
|
||||||
// Good - no dynamic allocation
|
|
||||||
std::array<int, MAX_VALUES> values;
|
|
||||||
```
|
|
||||||
Use `cg.add_define("MAX_VALUES", count)` to set the size from Python configuration.
|
|
||||||
|
|
||||||
**For byte buffers:** Avoid `std::vector<uint8_t>` unless the buffer needs to grow. Use `std::unique_ptr<uint8_t[]>` instead.
|
|
||||||
|
|
||||||
> **Note:** `std::unique_ptr<uint8_t[]>` does **not** provide bounds checking or iterator support like `std::vector<uint8_t>`. Use it only when you do not need these features and want minimal overhead.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Bad - STL overhead for simple byte buffer
|
|
||||||
std::vector<uint8_t> buffer;
|
|
||||||
buffer.resize(256);
|
|
||||||
|
|
||||||
// Good - minimal overhead, single allocation
|
|
||||||
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(256);
|
|
||||||
// Or if size is constant:
|
|
||||||
std::array<uint8_t, 256> buffer;
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
|
||||||
// Bad - generates STL realloc code (_M_realloc_insert)
|
|
||||||
std::vector<ServiceRecord> services;
|
|
||||||
services.reserve(5); // Still includes reallocation machinery
|
|
||||||
|
|
||||||
// Good - compile-time fixed size, stack allocated, no reallocation machinery
|
|
||||||
StaticVector<ServiceRecord, MAX_SERVICES> services; // Allocates all MAX_SERVICES on stack
|
|
||||||
services.push_back(record1); // Tracks count but all slots allocated
|
|
||||||
```
|
|
||||||
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.
|
|
||||||
|
|
||||||
3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization.
|
|
||||||
```cpp
|
|
||||||
// Bad - generates STL realloc code (_M_realloc_insert)
|
|
||||||
std::vector<TxtRecord> txt_records;
|
|
||||||
txt_records.reserve(5); // Still includes reallocation machinery
|
|
||||||
|
|
||||||
// Good - runtime size, single allocation, no reallocation machinery
|
|
||||||
FixedVector<TxtRecord> txt_records;
|
|
||||||
txt_records.init(record_count); // Initialize with exact size at runtime
|
|
||||||
```
|
|
||||||
**Benefits:**
|
|
||||||
- Eliminates `_M_realloc_insert`, `_M_default_append` template instantiations (saves 200-500 bytes per instance)
|
|
||||||
- Single allocation, no upper bound needed
|
|
||||||
- No reallocation overhead
|
|
||||||
- Compatible with protobuf code generation when using `[(fixed_vector) = true]` option
|
|
||||||
|
|
||||||
4. **Small datasets (1-16 elements):** Use `std::vector` or `std::array` with simple structs instead of `std::map`/`std::set`/`std::unordered_map`.
|
|
||||||
```cpp
|
|
||||||
// Bad - 2KB+ overhead for red-black tree/hash table
|
|
||||||
std::map<std::string, int> small_lookup;
|
|
||||||
std::unordered_map<int, std::string> tiny_map;
|
|
||||||
|
|
||||||
// Good - simple struct with linear search (std::vector is fine)
|
|
||||||
struct LookupEntry {
|
|
||||||
const char *key;
|
|
||||||
int value;
|
|
||||||
};
|
|
||||||
std::vector<LookupEntry> small_lookup = {
|
|
||||||
{"key1", 10},
|
|
||||||
{"key2", 20},
|
|
||||||
{"key3", 30},
|
|
||||||
};
|
|
||||||
// Or std::array if size is compile-time constant:
|
|
||||||
// std::array<LookupEntry, 3> small_lookup = {{ ... }};
|
|
||||||
```
|
|
||||||
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. **Detection:** Look for these patterns in compiler output:
|
|
||||||
- Large code sections with STL symbols (vector, map, set)
|
|
||||||
- `alloc`, `realloc`, `dealloc` in symbol names
|
|
||||||
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
|
|
||||||
- Red-black tree code (`rb_tree`, `_Rb_tree`)
|
|
||||||
- Hash table infrastructure (`unordered_map`, `hash`)
|
|
||||||
|
|
||||||
**When to optimize:**
|
|
||||||
- Core components (API, network, logger)
|
|
||||||
- Widely-used components (mdns, wifi, ble)
|
|
||||||
- Components causing flash size complaints
|
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
**Bad Pattern (Module-Level Globals):**
|
|
||||||
```python
|
|
||||||
# Don't do this - state persists between compilation runs
|
|
||||||
_component_state = []
|
|
||||||
_use_feature = None
|
|
||||||
|
|
||||||
def enable_feature():
|
|
||||||
global _use_feature
|
|
||||||
_use_feature = True
|
|
||||||
```
|
|
||||||
|
|
||||||
**Good Pattern (CORE.data with Helpers):**
|
|
||||||
```python
|
|
||||||
from esphome.core import CORE
|
|
||||||
|
|
||||||
# Keys for CORE.data storage
|
|
||||||
COMPONENT_STATE_KEY = "my_component_state"
|
|
||||||
USE_FEATURE_KEY = "my_component_use_feature"
|
|
||||||
|
|
||||||
def _get_component_state() -> list:
|
|
||||||
"""Get component state from CORE.data."""
|
|
||||||
return CORE.data.setdefault(COMPONENT_STATE_KEY, [])
|
|
||||||
|
|
||||||
def _get_use_feature() -> bool | None:
|
|
||||||
"""Get feature flag from CORE.data."""
|
|
||||||
return CORE.data.get(USE_FEATURE_KEY)
|
|
||||||
|
|
||||||
def _set_use_feature(value: bool) -> None:
|
|
||||||
"""Set feature flag in CORE.data."""
|
|
||||||
CORE.data[USE_FEATURE_KEY] = value
|
|
||||||
|
|
||||||
def enable_feature():
|
|
||||||
_set_use_feature(True)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why this matters:**
|
|
||||||
- Module-level globals persist between compilation runs if the dashboard doesn't fork/exec
|
|
||||||
- `CORE.data` automatically clears between runs
|
|
||||||
- Typed helper functions provide better IDE support and maintainability
|
|
||||||
- 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.
|
||||||
|
|
||||||
@@ -440,45 +223,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
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c
|
499db61c1aa55b98b6629df603a56a1ba7aff5a9a7c781a5c1552a9dcd186c08
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
[run]
|
[run]
|
||||||
omit =
|
omit =
|
||||||
esphome/components/*
|
esphome/components/*
|
||||||
esphome/analyze_memory/*
|
|
||||||
tests/integration/*
|
tests/integration/*
|
||||||
|
|||||||
22
.github/workflows/auto-label-pr.yml
vendored
22
.github/workflows/auto-label-pr.yml
vendored
@@ -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',
|
||||||
@@ -141,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;
|
||||||
@@ -416,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;
|
||||||
|
|
||||||
@@ -430,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).`;
|
||||||
}
|
}
|
||||||
@@ -466,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)
|
||||||
);
|
);
|
||||||
@@ -531,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);
|
||||||
|
|
||||||
@@ -627,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'];
|
||||||
@@ -636,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) {
|
||||||
|
|||||||
2
.github/workflows/ci-api-proto.yml
vendored
2
.github/workflows/ci-api-proto.yml
vendored
@@ -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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: generated-proto-files
|
name: generated-proto-files
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
1
.github/workflows/ci-clang-tidy-hash.yml
vendored
1
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -6,7 +6,6 @@ on:
|
|||||||
- ".clang-tidy"
|
- ".clang-tidy"
|
||||||
- "platformio.ini"
|
- "platformio.ini"
|
||||||
- "requirements_dev.txt"
|
- "requirements_dev.txt"
|
||||||
- "sdkconfig.defaults"
|
|
||||||
- ".clang-tidy.hash"
|
- ".clang-tidy.hash"
|
||||||
- "script/clang_tidy_hash.py"
|
- "script/clang_tidy_hash.py"
|
||||||
- ".github/workflows/ci-clang-tidy-hash.yml"
|
- ".github/workflows/ci-clang-tidy-hash.yml"
|
||||||
|
|||||||
111
.github/workflows/ci-memory-impact-comment.yml
vendored
111
.github/workflows/ci-memory-impact-comment.yml
vendored
@@ -1,111 +0,0 @@
|
|||||||
---
|
|
||||||
name: Memory Impact Comment (Forks)
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["CI"]
|
|
||||||
types: [completed]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
actions: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
memory-impact-comment:
|
|
||||||
name: Post memory impact comment (fork PRs only)
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
# Only run for PRs from forks that had successful CI runs
|
|
||||||
if: >
|
|
||||||
github.event.workflow_run.event == 'pull_request' &&
|
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.event.workflow_run.head_repository.full_name != github.repository
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
steps:
|
|
||||||
- name: Get PR details
|
|
||||||
id: pr
|
|
||||||
run: |
|
|
||||||
# Get PR details by searching for PR with matching head SHA
|
|
||||||
# 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 }}"
|
|
||||||
pr_data=$(gh api --paginate "/repos/${{ github.repository }}/pulls" \
|
|
||||||
--jq ".[] | select(.head.sha == \"$head_sha\") | {number: .number, base_ref: .base.ref}" \
|
|
||||||
| head -n 1)
|
|
||||||
|
|
||||||
if [ -z "$pr_data" ]; then
|
|
||||||
echo "No PR found for SHA $head_sha, skipping"
|
|
||||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
pr_number=$(echo "$pr_data" | jq -r '.number')
|
|
||||||
base_ref=$(echo "$pr_data" | jq -r '.base_ref')
|
|
||||||
|
|
||||||
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Found PR #$pr_number targeting base branch: $base_ref"
|
|
||||||
|
|
||||||
- name: Check out code from base repository
|
|
||||||
if: steps.pr.outputs.skip != 'true'
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
with:
|
|
||||||
# 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
|
|
||||||
repository: ${{ github.repository }}
|
|
||||||
ref: ${{ steps.pr.outputs.base_ref }}
|
|
||||||
|
|
||||||
- name: Restore Python
|
|
||||||
if: steps.pr.outputs.skip != 'true'
|
|
||||||
uses: ./.github/actions/restore-python
|
|
||||||
with:
|
|
||||||
python-version: "3.11"
|
|
||||||
cache-key: ${{ hashFiles('.cache-key') }}
|
|
||||||
|
|
||||||
- name: Download memory analysis artifacts
|
|
||||||
if: steps.pr.outputs.skip != 'true'
|
|
||||||
run: |
|
|
||||||
run_id="${{ github.event.workflow_run.id }}"
|
|
||||||
echo "Downloading artifacts from workflow run $run_id"
|
|
||||||
|
|
||||||
mkdir -p memory-analysis
|
|
||||||
|
|
||||||
# Download target analysis artifact
|
|
||||||
if gh run download --name "memory-analysis-target" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
|
|
||||||
echo "Downloaded memory-analysis-target artifact."
|
|
||||||
else
|
|
||||||
echo "No memory-analysis-target artifact found."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Download PR analysis artifact
|
|
||||||
if gh run download --name "memory-analysis-pr" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
|
|
||||||
echo "Downloaded memory-analysis-pr artifact."
|
|
||||||
else
|
|
||||||
echo "No memory-analysis-pr artifact found."
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check if artifacts exist
|
|
||||||
id: check
|
|
||||||
if: steps.pr.outputs.skip != 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then
|
|
||||||
echo "found=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Memory analysis artifacts not found, skipping comment"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Post or update PR comment
|
|
||||||
if: steps.pr.outputs.skip != 'true' && steps.check.outputs.found == 'true'
|
|
||||||
env:
|
|
||||||
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
# Pass PR number and JSON file paths directly to Python script
|
|
||||||
# Let Python parse the JSON to avoid shell injection risks
|
|
||||||
# The script will validate and sanitize all inputs
|
|
||||||
python script/ci_memory_impact_comment.py \
|
|
||||||
--pr-number "$PR_NUMBER" \
|
|
||||||
--target-json ./memory-analysis/memory-analysis-target.json \
|
|
||||||
--pr-json ./memory-analysis/memory-analysis-pr.json
|
|
||||||
628
.github/workflows/ci.yml
vendored
628
.github/workflows/ci.yml
vendored
@@ -114,6 +114,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- "3.11"
|
- "3.11"
|
||||||
|
- "3.12"
|
||||||
- "3.13"
|
- "3.13"
|
||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
@@ -125,8 +126,12 @@ jobs:
|
|||||||
# 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.13"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
|
- python-version: "3.12"
|
||||||
|
os: windows-latest
|
||||||
- python-version: "3.13"
|
- python-version: "3.13"
|
||||||
os: macOS-latest
|
os: macOS-latest
|
||||||
|
- python-version: "3.12"
|
||||||
|
os: macOS-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
@@ -170,17 +175,9 @@ 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 }}
|
|
||||||
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 }}
|
|
||||||
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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -192,11 +189,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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
||||||
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 +202,9 @@ 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 "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 "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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
||||||
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
|
||||||
@@ -264,34 +242,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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
|
|
||||||
- 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 +260,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
|
||||||
@@ -389,192 +356,79 @@ jobs:
|
|||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
clang-tidy-nosplit:
|
test-build-components:
|
||||||
name: Run script/clang-tidy for ESP32 Arduino
|
name: Component test ${{ matrix.file }}
|
||||||
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 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
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
|
|
||||||
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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
key: platformio-tidyesp32-${{ 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-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 --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
|
|
||||||
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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 2
|
max-parallel: 2
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }}
|
||||||
- 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:
|
steps:
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install libsdl2-dev
|
||||||
|
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
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: test_build_components -e config -c ${{ matrix.file }}
|
||||||
- name: Cache platformio
|
|
||||||
if: github.ref == 'refs/heads/dev'
|
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
key: platformio-tidyesp32-${{ 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-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
|
./script/test_build_components -e config -c ${{ matrix.file }}
|
||||||
echo "full_scan=true" >> $GITHUB_OUTPUT
|
- name: test_build_components -e compile -c ${{ matrix.file }}
|
||||||
echo "reason=hash_changed" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "full_scan=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "reason=normal" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run clang-tidy
|
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
./script/test_build_components -e compile -c ${{ matrix.file }}
|
||||||
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
|
test-build-components-splitter:
|
||||||
run: script/ci-suggest-changes
|
name: Split components for testing into 20 groups maximum
|
||||||
if: always()
|
|
||||||
|
|
||||||
test-build-components-split:
|
|
||||||
name: Test components batch (${{ matrix.components }})
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
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) >= 100
|
||||||
|
outputs:
|
||||||
|
matrix: ${{ steps.split.outputs.components }}
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
- name: Split components into 20 groups
|
||||||
|
id: split
|
||||||
|
run: |
|
||||||
|
components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
|
||||||
|
echo "components=$components" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
test-build-components-split:
|
||||||
|
name: Test split components
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
- determine-jobs
|
||||||
|
- test-build-components-splitter
|
||||||
|
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
|
max-parallel: 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
|
|
||||||
run: |
|
|
||||||
echo "Available disk space:"
|
|
||||||
df -h
|
|
||||||
|
|
||||||
- name: List components
|
- name: List components
|
||||||
run: echo ${{ matrix.components }}
|
run: echo ${{ matrix.components }}
|
||||||
|
|
||||||
- name: Cache apt packages
|
- name: Install dependencies
|
||||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
|
run: |
|
||||||
with:
|
sudo apt-get update
|
||||||
packages: libsdl2-dev
|
sudo apt-get install libsdl2-dev
|
||||||
version: 1.0
|
|
||||||
|
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -583,83 +437,27 @@ 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: Validate and compile components with intelligent grouping
|
- name: Validate config
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
for component in ${{ matrix.components }}; do
|
||||||
# Check if /mnt has more free space than / before bind mounting
|
./script/test_build_components -e config -c $component
|
||||||
# Extract available space in KB for comparison
|
done
|
||||||
root_avail=$(df -k / | awk 'NR==2 {print $4}')
|
- name: Compile config
|
||||||
mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}')
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB"
|
mkdir build_cache
|
||||||
|
export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache
|
||||||
# Only use /mnt if it has more space than /
|
for component in ${{ matrix.components }}; do
|
||||||
if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
|
./script/test_build_components -e compile -c $component
|
||||||
echo "Using /mnt for build files (more space available)"
|
done
|
||||||
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
|
|
||||||
sudo mkdir -p /mnt/platformio
|
|
||||||
sudo chown $USER:$USER /mnt/platformio
|
|
||||||
mkdir -p ~/.platformio
|
|
||||||
sudo mount --bind /mnt/platformio ~/.platformio
|
|
||||||
|
|
||||||
# Bind mount test build directory to /mnt
|
|
||||||
sudo mkdir -p /mnt/test_build_components_build
|
|
||||||
sudo chown $USER:$USER /mnt/test_build_components_build
|
|
||||||
mkdir -p tests/test_build_components/build
|
|
||||||
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
|
|
||||||
else
|
|
||||||
echo "Using / for build files (more space available than /mnt or /mnt unavailable)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Convert space-separated components to comma-separated for Python script
|
|
||||||
components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
|
|
||||||
|
|
||||||
# Only isolate directly changed components when targeting dev branch
|
|
||||||
# For beta/release branches, group everything for faster CI
|
|
||||||
#
|
|
||||||
# WHY ISOLATE DIRECTLY CHANGED COMPONENTS?
|
|
||||||
# - Isolated tests run WITHOUT --testing-mode, enabling full validation
|
|
||||||
# - This catches pin conflicts and other issues in directly changed code
|
|
||||||
# - Grouped tests use --testing-mode to allow config merging (disables some checks)
|
|
||||||
# - Dependencies are safe to group since they weren't modified in this PR
|
|
||||||
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
|
|
||||||
directly_changed_csv=""
|
|
||||||
echo "Testing components: $components_csv"
|
|
||||||
echo "Target branch: ${{ github.base_ref }} - grouping all components"
|
|
||||||
else
|
|
||||||
directly_changed_csv=$(echo '${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}' | jq -r 'join(",")')
|
|
||||||
echo "Testing components: $components_csv"
|
|
||||||
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components: $directly_changed_csv"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show disk space before validation (after bind mounts setup)
|
|
||||||
echo "Disk space before config validation:"
|
|
||||||
df -h
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Run config validation with grouping and isolation
|
|
||||||
python3 script/test_build_components.py -e config -c "$components_csv" -f --isolate "$directly_changed_csv"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Config validation passed! Starting compilation..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Show disk space before compilation
|
|
||||||
echo "Disk space before compilation:"
|
|
||||||
df -h
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Run compilation with grouping and isolation
|
|
||||||
python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv"
|
|
||||||
|
|
||||||
pre-commit-ci-lite:
|
pre-commit-ci-lite:
|
||||||
name: pre-commit.ci lite
|
name: pre-commit.ci lite
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
|
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -674,271 +472,6 @@ jobs:
|
|||||||
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
memory-impact-target-branch:
|
|
||||||
name: Build target branch for memory impact
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
|
||||||
- common
|
|
||||||
- determine-jobs
|
|
||||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true'
|
|
||||||
outputs:
|
|
||||||
ram_usage: ${{ steps.extract.outputs.ram_usage }}
|
|
||||||
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
|
||||||
cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }}
|
|
||||||
skip: ${{ steps.check-script.outputs.skip }}
|
|
||||||
steps:
|
|
||||||
- name: Check out target branch
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
with:
|
|
||||||
ref: ${{ github.base_ref }}
|
|
||||||
|
|
||||||
# Check if memory impact extraction script exists on target branch
|
|
||||||
# If not, skip the analysis (this handles older branches that don't have the feature)
|
|
||||||
- name: Check for memory impact script
|
|
||||||
id: check-script
|
|
||||||
run: |
|
|
||||||
if [ -f "script/ci_memory_impact_extract.py" ]; then
|
|
||||||
echo "skip=false" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "skip=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# All remaining steps only run if script exists
|
|
||||||
- name: Generate cache key
|
|
||||||
id: cache-key
|
|
||||||
if: steps.check-script.outputs.skip != 'true'
|
|
||||||
run: |
|
|
||||||
# Get the commit SHA of the target branch
|
|
||||||
target_sha=$(git rev-parse HEAD)
|
|
||||||
|
|
||||||
# Hash the build infrastructure files (all files that affect build/analysis)
|
|
||||||
infra_hash=$(cat \
|
|
||||||
script/test_build_components.py \
|
|
||||||
script/ci_memory_impact_extract.py \
|
|
||||||
script/analyze_component_buses.py \
|
|
||||||
script/merge_component_configs.py \
|
|
||||||
script/ci_helpers.py \
|
|
||||||
.github/workflows/ci.yml \
|
|
||||||
| sha256sum | cut -d' ' -f1)
|
|
||||||
|
|
||||||
# Get platform and components from job inputs
|
|
||||||
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
|
|
||||||
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
|
|
||||||
components_hash=$(echo "$components" | sha256sum | cut -d' ' -f1)
|
|
||||||
|
|
||||||
# Combine into cache key
|
|
||||||
cache_key="memory-analysis-target-${target_sha}-${infra_hash}-${platform}-${components_hash}"
|
|
||||||
echo "cache-key=${cache_key}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Cache key: ${cache_key}"
|
|
||||||
|
|
||||||
- name: Restore cached memory analysis
|
|
||||||
id: cache-memory-analysis
|
|
||||||
if: steps.check-script.outputs.skip != 'true'
|
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
||||||
with:
|
|
||||||
path: memory-analysis-target.json
|
|
||||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
|
||||||
|
|
||||||
- name: Cache status
|
|
||||||
if: steps.check-script.outputs.skip != 'true'
|
|
||||||
run: |
|
|
||||||
if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then
|
|
||||||
echo "✓ Cache hit! Using cached memory analysis results."
|
|
||||||
echo " Skipping build step to save time."
|
|
||||||
else
|
|
||||||
echo "✗ Cache miss. Will build and analyze memory usage."
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Restore Python
|
|
||||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
|
||||||
uses: ./.github/actions/restore-python
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
|
||||||
|
|
||||||
- name: Cache platformio
|
|
||||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
|
||||||
|
|
||||||
- name: Build, compile, and analyze memory
|
|
||||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
|
||||||
id: build
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
|
|
||||||
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
|
|
||||||
|
|
||||||
echo "Building with test_build_components.py for $platform with components:"
|
|
||||||
echo "$components" | jq -r '.[]' | sed 's/^/ - /'
|
|
||||||
|
|
||||||
# Use test_build_components.py which handles grouping automatically
|
|
||||||
# Pass components as comma-separated list
|
|
||||||
component_list=$(echo "$components" | jq -r 'join(",")')
|
|
||||||
|
|
||||||
echo "Compiling with test_build_components.py..."
|
|
||||||
|
|
||||||
# Run build and extract memory with auto-detection of build directory for detailed analysis
|
|
||||||
# Use tee to show output in CI while also piping to extraction script
|
|
||||||
python script/test_build_components.py \
|
|
||||||
-e compile \
|
|
||||||
-c "$component_list" \
|
|
||||||
-t "$platform" 2>&1 | \
|
|
||||||
tee /dev/stderr | \
|
|
||||||
python script/ci_memory_impact_extract.py \
|
|
||||||
--output-env \
|
|
||||||
--output-json memory-analysis-target.json
|
|
||||||
|
|
||||||
# Add metadata to JSON before caching
|
|
||||||
python script/ci_add_metadata_to_json.py \
|
|
||||||
--json-file memory-analysis-target.json \
|
|
||||||
--components "$components" \
|
|
||||||
--platform "$platform"
|
|
||||||
|
|
||||||
- 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'
|
|
||||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
||||||
with:
|
|
||||||
path: memory-analysis-target.json
|
|
||||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
|
||||||
|
|
||||||
- name: Extract memory usage for outputs
|
|
||||||
id: extract
|
|
||||||
if: steps.check-script.outputs.skip != 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f memory-analysis-target.json ]; then
|
|
||||||
ram=$(jq -r '.ram_bytes' memory-analysis-target.json)
|
|
||||||
flash=$(jq -r '.flash_bytes' memory-analysis-target.json)
|
|
||||||
echo "ram_usage=${ram}" >> $GITHUB_OUTPUT
|
|
||||||
echo "flash_usage=${flash}" >> $GITHUB_OUTPUT
|
|
||||||
echo "RAM: ${ram} bytes, Flash: ${flash} bytes"
|
|
||||||
else
|
|
||||||
echo "Error: memory-analysis-target.json not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload memory analysis JSON
|
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
with:
|
|
||||||
name: memory-analysis-target
|
|
||||||
path: memory-analysis-target.json
|
|
||||||
if-no-files-found: warn
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
memory-impact-pr-branch:
|
|
||||||
name: Build PR branch for memory impact
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
|
||||||
- common
|
|
||||||
- determine-jobs
|
|
||||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true'
|
|
||||||
outputs:
|
|
||||||
ram_usage: ${{ steps.extract.outputs.ram_usage }}
|
|
||||||
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
|
||||||
steps:
|
|
||||||
- name: Check out PR branch
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
- name: Restore Python
|
|
||||||
uses: ./.github/actions/restore-python
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
|
||||||
- name: Cache platformio
|
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
|
||||||
- name: Build, compile, and analyze memory
|
|
||||||
id: extract
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
|
|
||||||
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
|
|
||||||
|
|
||||||
echo "Building with test_build_components.py for $platform with components:"
|
|
||||||
echo "$components" | jq -r '.[]' | sed 's/^/ - /'
|
|
||||||
|
|
||||||
# Use test_build_components.py which handles grouping automatically
|
|
||||||
# Pass components as comma-separated list
|
|
||||||
component_list=$(echo "$components" | jq -r 'join(",")')
|
|
||||||
|
|
||||||
echo "Compiling with test_build_components.py..."
|
|
||||||
|
|
||||||
# Run build and extract memory with auto-detection of build directory for detailed analysis
|
|
||||||
# Use tee to show output in CI while also piping to extraction script
|
|
||||||
python script/test_build_components.py \
|
|
||||||
-e compile \
|
|
||||||
-c "$component_list" \
|
|
||||||
-t "$platform" 2>&1 | \
|
|
||||||
tee /dev/stderr | \
|
|
||||||
python script/ci_memory_impact_extract.py \
|
|
||||||
--output-env \
|
|
||||||
--output-json memory-analysis-pr.json
|
|
||||||
|
|
||||||
# Add metadata to JSON (components and platform are in shell variables above)
|
|
||||||
python script/ci_add_metadata_to_json.py \
|
|
||||||
--json-file memory-analysis-pr.json \
|
|
||||||
--components "$components" \
|
|
||||||
--platform "$platform"
|
|
||||||
|
|
||||||
- name: Upload memory analysis JSON
|
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
with:
|
|
||||||
name: memory-analysis-pr
|
|
||||||
path: memory-analysis-pr.json
|
|
||||||
if-no-files-found: warn
|
|
||||||
retention-days: 1
|
|
||||||
|
|
||||||
memory-impact-comment:
|
|
||||||
name: Comment memory impact
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
|
||||||
- common
|
|
||||||
- determine-jobs
|
|
||||||
- memory-impact-target-branch
|
|
||||||
- memory-impact-pr-branch
|
|
||||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
steps:
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
- name: Restore Python
|
|
||||||
uses: ./.github/actions/restore-python
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
|
||||||
- name: Download target analysis JSON
|
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
|
||||||
with:
|
|
||||||
name: memory-analysis-target
|
|
||||||
path: ./memory-analysis
|
|
||||||
continue-on-error: true
|
|
||||||
- name: Download PR analysis JSON
|
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
|
||||||
with:
|
|
||||||
name: memory-analysis-pr
|
|
||||||
path: ./memory-analysis
|
|
||||||
continue-on-error: true
|
|
||||||
- name: Post or update PR comment
|
|
||||||
env:
|
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
|
|
||||||
# Pass JSON file paths directly to Python script
|
|
||||||
# All data is extracted from JSON files for security
|
|
||||||
python script/ci_memory_impact_comment.py \
|
|
||||||
--pr-number "$PR_NUMBER" \
|
|
||||||
--target-json ./memory-analysis/memory-analysis-target.json \
|
|
||||||
--pr-json ./memory-analysis/memory-analysis-pr.json
|
|
||||||
|
|
||||||
ci-status:
|
ci-status:
|
||||||
name: CI Status
|
name: CI Status
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@@ -948,15 +481,12 @@ 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
|
||||||
|
- 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-pr-branch
|
|
||||||
- memory-impact-comment
|
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- name: Success
|
- name: Success
|
||||||
|
|||||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||||
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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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
|
||||||
@@ -171,7 +171,7 @@ jobs:
|
|||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
|
|||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
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
|
||||||
operations-per-run: 400
|
operations-per-run: 150
|
||||||
|
|
||||||
# The 90 day stale policy for PRs
|
# The 90 day stale policy for PRs
|
||||||
# - PRs
|
# - PRs
|
||||||
|
|||||||
1
.github/workflows/status-check-labels.yml
vendored
1
.github/workflows/status-check-labels.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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.4
|
rev: v0.13.3
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|||||||
12
CODEOWNERS
12
CODEOWNERS
@@ -62,7 +62,6 @@ esphome/components/bedjet/fan/* @jhansche
|
|||||||
esphome/components/bedjet/sensor/* @javawizard @jhansche
|
esphome/components/bedjet/sensor/* @javawizard @jhansche
|
||||||
esphome/components/beken_spi_led_strip/* @Mat931
|
esphome/components/beken_spi_led_strip/* @Mat931
|
||||||
esphome/components/bh1750/* @OttoWinter
|
esphome/components/bh1750/* @OttoWinter
|
||||||
esphome/components/bh1900nux/* @B48D81EFCC
|
|
||||||
esphome/components/binary_sensor/* @esphome/core
|
esphome/components/binary_sensor/* @esphome/core
|
||||||
esphome/components/bk72xx/* @kuba2k2
|
esphome/components/bk72xx/* @kuba2k2
|
||||||
esphome/components/bl0906/* @athom-tech @jesserockz @tarontop
|
esphome/components/bl0906/* @athom-tech @jesserockz @tarontop
|
||||||
@@ -70,7 +69,6 @@ esphome/components/bl0939/* @ziceva
|
|||||||
esphome/components/bl0940/* @dan-s-github @tobias-
|
esphome/components/bl0940/* @dan-s-github @tobias-
|
||||||
esphome/components/bl0942/* @dbuezas @dwmw2
|
esphome/components/bl0942/* @dbuezas @dwmw2
|
||||||
esphome/components/ble_client/* @buxtronix @clydebarrow
|
esphome/components/ble_client/* @buxtronix @clydebarrow
|
||||||
esphome/components/ble_nus/* @tomaszduda23
|
|
||||||
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
|
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
|
||||||
esphome/components/bme280_base/* @esphome/core
|
esphome/components/bme280_base/* @esphome/core
|
||||||
esphome/components/bme280_spi/* @apbodrov
|
esphome/components/bme280_spi/* @apbodrov
|
||||||
@@ -141,7 +139,6 @@ esphome/components/ens160_base/* @latonita @vincentscode
|
|||||||
esphome/components/ens160_i2c/* @latonita
|
esphome/components/ens160_i2c/* @latonita
|
||||||
esphome/components/ens160_spi/* @latonita
|
esphome/components/ens160_spi/* @latonita
|
||||||
esphome/components/ens210/* @itn3rd77
|
esphome/components/ens210/* @itn3rd77
|
||||||
esphome/components/epaper_spi/* @esphome/core
|
|
||||||
esphome/components/es7210/* @kahrendt
|
esphome/components/es7210/* @kahrendt
|
||||||
esphome/components/es7243e/* @kbx81
|
esphome/components/es7243e/* @kbx81
|
||||||
esphome/components/es8156/* @kbx81
|
esphome/components/es8156/* @kbx81
|
||||||
@@ -155,14 +152,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
|
||||||
@@ -181,7 +176,7 @@ 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
|
||||||
@@ -202,7 +197,6 @@ 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/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
|
||||||
@@ -262,7 +256,6 @@ esphome/components/libretiny_pwm/* @kuba2k2
|
|||||||
esphome/components/light/* @esphome/core
|
esphome/components/light/* @esphome/core
|
||||||
esphome/components/lightwaverf/* @max246
|
esphome/components/lightwaverf/* @max246
|
||||||
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
|
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
|
||||||
esphome/components/lm75b/* @beormund
|
|
||||||
esphome/components/ln882x/* @lamauny
|
esphome/components/ln882x/* @lamauny
|
||||||
esphome/components/lock/* @esphome/core
|
esphome/components/lock/* @esphome/core
|
||||||
esphome/components/logger/* @esphome/core
|
esphome/components/logger/* @esphome/core
|
||||||
@@ -290,7 +283,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
|
||||||
@@ -436,7 +428,6 @@ esphome/components/speaker/media_player/* @kahrendt @synesthesiam
|
|||||||
esphome/components/spi/* @clydebarrow @esphome/core
|
esphome/components/spi/* @clydebarrow @esphome/core
|
||||||
esphome/components/spi_device/* @clydebarrow
|
esphome/components/spi_device/* @clydebarrow
|
||||||
esphome/components/spi_led_strip/* @clydebarrow
|
esphome/components/spi_led_strip/* @clydebarrow
|
||||||
esphome/components/split_buffer/* @jesserockz
|
|
||||||
esphome/components/sprinkler/* @kbx81
|
esphome/components/sprinkler/* @kbx81
|
||||||
esphome/components/sps30/* @martgras
|
esphome/components/sps30/* @martgras
|
||||||
esphome/components/ssd1322_base/* @kbx81
|
esphome/components/ssd1322_base/* @kbx81
|
||||||
@@ -481,7 +472,6 @@ esphome/components/template/fan/* @ssieb
|
|||||||
esphome/components/text/* @mauritskorse
|
esphome/components/text/* @mauritskorse
|
||||||
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
|
||||||
|
|||||||
2
Doxyfile
2
Doxyfile
@@ -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 = 2025.11.0-dev
|
PROJECT_NUMBER = 2025.10.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
|
||||||
|
|||||||
@@ -62,40 +62,6 @@ from esphome.util import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# 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
|
||||||
@@ -151,17 +117,6 @@ class Purpose(StrEnum):
|
|||||||
LOGGING = "logging"
|
LOGGING = "logging"
|
||||||
|
|
||||||
|
|
||||||
class PortType(StrEnum):
|
|
||||||
SERIAL = "SERIAL"
|
|
||||||
NETWORK = "NETWORK"
|
|
||||||
MQTT = "MQTT"
|
|
||||||
MQTTIP = "MQTTIP"
|
|
||||||
|
|
||||||
|
|
||||||
# Magic MQTT port types that require special handling
|
|
||||||
_MQTT_PORT_TYPES = frozenset({PortType.MQTT, PortType.MQTTIP})
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]:
|
def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]:
|
||||||
"""Resolve an address using cache if available, otherwise return the address itself."""
|
"""Resolve an address using cache if available, otherwise return the address itself."""
|
||||||
if CORE.address_cache and (cached := CORE.address_cache.get_addresses(address)):
|
if CORE.address_cache and (cached := CORE.address_cache.get_addresses(address)):
|
||||||
@@ -207,21 +162,19 @@ 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)
|
||||||
if not resolved:
|
if not resolved:
|
||||||
raise EsphomeError(
|
_LOGGER.error("All specified devices: %s could not be resolved.", defaults)
|
||||||
f"All specified devices {defaults} could not be resolved. Is the device connected to the network?"
|
|
||||||
)
|
|
||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
# No devices specified, show interactive chooser
|
# No devices specified, show interactive chooser
|
||||||
@@ -315,20 +268,8 @@ def has_ip_address() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def has_resolvable_address() -> bool:
|
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 or is an IP address)."""
|
||||||
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
|
return has_mdns() or has_ip_address()
|
||||||
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
|
|
||||||
if CORE.address is 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):
|
||||||
@@ -337,67 +278,16 @@ def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str
|
|||||||
return mqtt.get_esphome_device_ip(config, username, password, client_id)
|
return mqtt.get_esphome_device_ip(config, username, password, client_id)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_network_devices(
|
_PORT_TO_PORT_TYPE = {
|
||||||
devices: list[str], config: ConfigType, args: ArgsProtocol
|
"MQTT": "MQTT",
|
||||||
) -> list[str]:
|
"MQTTIP": "MQTTIP",
|
||||||
"""Resolve device list, converting MQTT magic strings to actual IP addresses.
|
}
|
||||||
|
|
||||||
This function filters the devices list to:
|
|
||||||
- Replace MQTT/MQTTIP magic strings with actual IP addresses via MQTT lookup
|
|
||||||
- Deduplicate addresses while preserving order
|
|
||||||
- Only resolve MQTT once even if multiple MQTT strings are present
|
|
||||||
- If MQTT resolution fails, log a warning and continue with other devices
|
|
||||||
|
|
||||||
Args:
|
|
||||||
devices: List of device identifiers (IPs, hostnames, or magic strings)
|
|
||||||
config: ESPHome configuration
|
|
||||||
args: Command-line arguments containing MQTT credentials
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of network addresses suitable for connection attempts
|
|
||||||
"""
|
|
||||||
network_devices: list[str] = []
|
|
||||||
mqtt_resolved: bool = False
|
|
||||||
|
|
||||||
for device in devices:
|
|
||||||
port_type = get_port_type(device)
|
|
||||||
if port_type in _MQTT_PORT_TYPES:
|
|
||||||
# Only resolve MQTT once, even if multiple MQTT entries
|
|
||||||
if not mqtt_resolved:
|
|
||||||
try:
|
|
||||||
mqtt_ips = mqtt_get_ip(
|
|
||||||
config, args.username, args.password, args.client_id
|
|
||||||
)
|
|
||||||
network_devices.extend(mqtt_ips)
|
|
||||||
except EsphomeError as err:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"MQTT IP discovery failed (%s), will try other devices if available",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
mqtt_resolved = True
|
|
||||||
elif device not in network_devices:
|
|
||||||
# Regular network address or IP - add if not already present
|
|
||||||
network_devices.append(device)
|
|
||||||
|
|
||||||
return network_devices
|
|
||||||
|
|
||||||
|
|
||||||
def get_port_type(port: str) -> PortType:
|
def get_port_type(port: str) -> str:
|
||||||
"""Determine the type of port/device identifier.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.)
|
|
||||||
PortType.MQTT for MQTT logging
|
|
||||||
PortType.MQTTIP for MQTT IP lookup
|
|
||||||
PortType.NETWORK for IP addresses, hostnames, or mDNS names
|
|
||||||
"""
|
|
||||||
if port.startswith("/") or port.startswith("COM"):
|
if port.startswith("/") or port.startswith("COM"):
|
||||||
return PortType.SERIAL
|
return "SERIAL"
|
||||||
if port == "MQTT":
|
return _PORT_TO_PORT_TYPE.get(port, "NETWORK")
|
||||||
return PortType.MQTT
|
|
||||||
if port == "MQTTIP":
|
|
||||||
return PortType.MQTTIP
|
|
||||||
return PortType.NETWORK
|
|
||||||
|
|
||||||
|
|
||||||
def run_miniterm(config: ConfigType, port: str, args) -> int:
|
def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||||
@@ -512,9 +402,7 @@ def write_cpp_file() -> int:
|
|||||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||||
from esphome import platformio_api
|
from esphome import platformio_api
|
||||||
|
|
||||||
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
|
_LOGGER.info("Compiling app...")
|
||||||
# If you change this format, update the regex in that script as well
|
|
||||||
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
|
|
||||||
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
|
||||||
@@ -599,7 +487,7 @@ def upload_using_platformio(config: ConfigType, port: str):
|
|||||||
|
|
||||||
|
|
||||||
def check_permissions(port: str):
|
def check_permissions(port: str):
|
||||||
if os.name == "posix" and get_port_type(port) == PortType.SERIAL:
|
if os.name == "posix" and get_port_type(port) == "SERIAL":
|
||||||
# Check if we can open selected serial port
|
# Check if we can open selected serial port
|
||||||
if not os.access(port, os.F_OK):
|
if not os.access(port, os.F_OK):
|
||||||
raise EsphomeError(
|
raise EsphomeError(
|
||||||
@@ -627,7 +515,7 @@ def upload_program(
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if get_port_type(host) == PortType.SERIAL:
|
if get_port_type(host) == "SERIAL":
|
||||||
check_permissions(host)
|
check_permissions(host)
|
||||||
|
|
||||||
exit_code = 1
|
exit_code = 1
|
||||||
@@ -654,16 +542,17 @@ def upload_program(
|
|||||||
from esphome import espota2
|
from esphome import espota2
|
||||||
|
|
||||||
remote_port = int(ota_conf[CONF_PORT])
|
remote_port = int(ota_conf[CONF_PORT])
|
||||||
password = ota_conf.get(CONF_PASSWORD)
|
password = ota_conf.get(CONF_PASSWORD, "")
|
||||||
if getattr(args, "file", None) is not None:
|
if getattr(args, "file", None) is not None:
|
||||||
binary = Path(args.file)
|
binary = Path(args.file)
|
||||||
else:
|
else:
|
||||||
binary = CORE.firmware_bin
|
binary = CORE.firmware_bin
|
||||||
|
|
||||||
# Resolve MQTT magic strings to actual IP addresses
|
# MQTT address resolution
|
||||||
network_devices = _resolve_network_devices(devices, config, args)
|
if get_port_type(host) in ("MQTT", "MQTTIP"):
|
||||||
|
devices = mqtt_get_ip(config, args.username, args.password, args.client_id)
|
||||||
|
|
||||||
return espota2.run_ota(network_devices, remote_port, password, binary)
|
return espota2.run_ota(devices, remote_port, password, binary)
|
||||||
|
|
||||||
|
|
||||||
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
|
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
|
||||||
@@ -678,22 +567,32 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
|
|||||||
raise EsphomeError("Logger is not configured!")
|
raise EsphomeError("Logger is not configured!")
|
||||||
|
|
||||||
port = devices[0]
|
port = devices[0]
|
||||||
port_type = get_port_type(port)
|
|
||||||
|
|
||||||
if port_type == PortType.SERIAL:
|
if get_port_type(port) == "SERIAL":
|
||||||
check_permissions(port)
|
check_permissions(port)
|
||||||
return run_miniterm(config, port, args)
|
return run_miniterm(config, port, args)
|
||||||
|
|
||||||
|
port_type = get_port_type(port)
|
||||||
|
|
||||||
# Check if we should use API for logging
|
# Check if we should use API for logging
|
||||||
# Resolve MQTT magic strings to actual IP addresses
|
if has_api():
|
||||||
if has_api() and (
|
addresses_to_use: list[str] | None = None
|
||||||
network_devices := _resolve_network_devices(devices, config, args)
|
|
||||||
):
|
|
||||||
from esphome.components.api.client import run_logs
|
|
||||||
|
|
||||||
return run_logs(config, network_devices)
|
if port_type == "NETWORK" and (has_mdns() or is_ip_address(port)):
|
||||||
|
addresses_to_use = devices
|
||||||
|
elif port_type in ("NETWORK", "MQTT", "MQTTIP") and has_mqtt_ip_lookup():
|
||||||
|
# Only use MQTT IP lookup if the first condition didn't match
|
||||||
|
# (for MQTT/MQTTIP types, or for NETWORK when mdns/ip check fails)
|
||||||
|
addresses_to_use = mqtt_get_ip(
|
||||||
|
config, args.username, args.password, args.client_id
|
||||||
|
)
|
||||||
|
|
||||||
if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging():
|
if addresses_to_use is not None:
|
||||||
|
from esphome.components.api.client import run_logs
|
||||||
|
|
||||||
|
return run_logs(config, addresses_to_use)
|
||||||
|
|
||||||
|
if port_type in ("NETWORK", "MQTT") and has_mqtt_logging():
|
||||||
from esphome import mqtt
|
from esphome import mqtt
|
||||||
|
|
||||||
return mqtt.show_logs(
|
return mqtt.show_logs(
|
||||||
@@ -936,54 +835,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
|
|
||||||
|
|
||||||
# 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 memory analysis
|
|
||||||
_LOGGER.info("Analyzing memory usage...")
|
|
||||||
analyzer = MemoryAnalyzerCLI(
|
|
||||||
str(firmware_elf),
|
|
||||||
idedata.objdump_path,
|
|
||||||
idedata.readelf_path,
|
|
||||||
external_components,
|
|
||||||
)
|
|
||||||
analyzer.analyze()
|
|
||||||
|
|
||||||
# Generate and display report
|
|
||||||
report = analyzer.generate_report()
|
|
||||||
print()
|
|
||||||
print(report)
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -1099,7 +950,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 = [
|
||||||
@@ -1152,12 +1002,6 @@ def parse_args(argv):
|
|||||||
action="append",
|
action="append",
|
||||||
default=[],
|
default=[],
|
||||||
)
|
)
|
||||||
options_parser.add_argument(
|
|
||||||
"--testing-mode",
|
|
||||||
help="Enable testing mode (disables validation checks for grouped component testing)",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=f"ESPHome {const.__version__}", parents=[options_parser]
|
description=f"ESPHome {const.__version__}", parents=[options_parser]
|
||||||
@@ -1385,14 +1229,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>.
|
||||||
#
|
#
|
||||||
@@ -1424,7 +1260,6 @@ def run_esphome(argv):
|
|||||||
|
|
||||||
args = parse_args(argv)
|
args = parse_args(argv)
|
||||||
CORE.dashboard = args.dashboard
|
CORE.dashboard = args.dashboard
|
||||||
CORE.testing_mode = args.testing_mode
|
|
||||||
|
|
||||||
# Create address cache from command-line arguments
|
# Create address cache from command-line arguments
|
||||||
CORE.address_cache = AddressCache.from_cli_args(
|
CORE.address_cache = AddressCache.from_cli_args(
|
||||||
|
|||||||
@@ -1,502 +0,0 @@
|
|||||||
"""Memory usage analyzer for ESPHome compiled binaries."""
|
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from .const import (
|
|
||||||
CORE_SUBCATEGORY_PATTERNS,
|
|
||||||
DEMANGLED_PATTERNS,
|
|
||||||
ESPHOME_COMPONENT_PATTERN,
|
|
||||||
SECTION_TO_ATTR,
|
|
||||||
SYMBOL_PATTERNS,
|
|
||||||
)
|
|
||||||
from .helpers import (
|
|
||||||
get_component_class_patterns,
|
|
||||||
get_esphome_components,
|
|
||||||
map_section_name,
|
|
||||||
parse_symbol_line,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from esphome.platformio_api import IDEData
|
|
||||||
|
|
||||||
_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
|
|
||||||
_CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"])
|
|
||||||
|
|
||||||
# libc printf/scanf family base names (used to detect variants like _printf_r, vfprintf, etc.)
|
|
||||||
_LIBC_PRINTF_SCANF_FAMILY = frozenset(["printf", "fprintf", "sprintf", "scanf"])
|
|
||||||
|
|
||||||
# Regex pattern for parsing readelf section headers
|
|
||||||
# Format: [ #] name type addr off size
|
|
||||||
_READELF_SECTION_PATTERN = re.compile(
|
|
||||||
r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Component category prefixes
|
|
||||||
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
|
|
||||||
_COMPONENT_PREFIX_EXTERNAL = "[external]"
|
|
||||||
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
|
|
||||||
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
|
|
||||||
|
|
||||||
# C++ namespace prefixes
|
|
||||||
_NAMESPACE_ESPHOME = "esphome::"
|
|
||||||
_NAMESPACE_STD = "std::"
|
|
||||||
|
|
||||||
# Type alias for symbol information: (symbol_name, size, component)
|
|
||||||
SymbolInfoType = tuple[str, int, str]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MemorySection:
|
|
||||||
"""Represents a memory section with its symbols."""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
symbols: list[SymbolInfoType] = field(default_factory=list)
|
|
||||||
total_size: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ComponentMemory:
|
|
||||||
"""Tracks memory usage for a component."""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
text_size: int = 0 # Code in flash
|
|
||||||
rodata_size: int = 0 # Read-only data in flash
|
|
||||||
data_size: int = 0 # Initialized data (flash + ram)
|
|
||||||
bss_size: int = 0 # Uninitialized data (ram only)
|
|
||||||
symbol_count: int = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def flash_total(self) -> int:
|
|
||||||
"""Total flash usage (text + rodata + data)."""
|
|
||||||
return self.text_size + self.rodata_size + self.data_size
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ram_total(self) -> int:
|
|
||||||
"""Total RAM usage (data + bss)."""
|
|
||||||
return self.data_size + self.bss_size
|
|
||||||
|
|
||||||
|
|
||||||
class MemoryAnalyzer:
|
|
||||||
"""Analyzes memory usage from ELF files."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
elf_path: str,
|
|
||||||
objdump_path: str | None = None,
|
|
||||||
readelf_path: str | None = None,
|
|
||||||
external_components: set[str] | None = None,
|
|
||||||
idedata: "IDEData | None" = None,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize memory analyzer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
elf_path: Path to ELF file to analyze
|
|
||||||
objdump_path: Path to objdump binary (auto-detected from idedata if not provided)
|
|
||||||
readelf_path: Path to readelf binary (auto-detected from idedata if not provided)
|
|
||||||
external_components: Set of external component names
|
|
||||||
idedata: Optional PlatformIO IDEData object to auto-detect toolchain paths
|
|
||||||
"""
|
|
||||||
self.elf_path = Path(elf_path)
|
|
||||||
if not self.elf_path.exists():
|
|
||||||
raise FileNotFoundError(f"ELF file not found: {elf_path}")
|
|
||||||
|
|
||||||
# Auto-detect toolchain paths from idedata if not provided
|
|
||||||
if idedata is not None and (objdump_path is None or readelf_path is None):
|
|
||||||
objdump_path = objdump_path or idedata.objdump_path
|
|
||||||
readelf_path = readelf_path or idedata.readelf_path
|
|
||||||
_LOGGER.debug("Using toolchain paths from PlatformIO idedata")
|
|
||||||
|
|
||||||
self.objdump_path = objdump_path or "objdump"
|
|
||||||
self.readelf_path = readelf_path or "readelf"
|
|
||||||
self.external_components = external_components or set()
|
|
||||||
|
|
||||||
self.sections: dict[str, MemorySection] = {}
|
|
||||||
self.components: dict[str, ComponentMemory] = defaultdict(
|
|
||||||
lambda: ComponentMemory("")
|
|
||||||
)
|
|
||||||
self._demangle_cache: dict[str, str] = {}
|
|
||||||
self._uncategorized_symbols: list[tuple[str, str, int]] = []
|
|
||||||
self._esphome_core_symbols: list[
|
|
||||||
tuple[str, str, int]
|
|
||||||
] = [] # Track core symbols
|
|
||||||
self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict(
|
|
||||||
list
|
|
||||||
) # Track symbols for all components
|
|
||||||
|
|
||||||
def analyze(self) -> dict[str, ComponentMemory]:
|
|
||||||
"""Analyze the ELF file and return component memory usage."""
|
|
||||||
self._parse_sections()
|
|
||||||
self._parse_symbols()
|
|
||||||
self._categorize_symbols()
|
|
||||||
return dict(self.components)
|
|
||||||
|
|
||||||
def _parse_sections(self) -> None:
|
|
||||||
"""Parse section headers from ELF file."""
|
|
||||||
result = subprocess.run(
|
|
||||||
[self.readelf_path, "-S", str(self.elf_path)],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Parse section headers
|
|
||||||
for line in result.stdout.splitlines():
|
|
||||||
# Look for section entries
|
|
||||||
if not (match := _READELF_SECTION_PATTERN.match(line)):
|
|
||||||
continue
|
|
||||||
|
|
||||||
section_name = match.group(1)
|
|
||||||
size_hex = match.group(2)
|
|
||||||
size = int(size_hex, 16)
|
|
||||||
|
|
||||||
# Map to standard section name
|
|
||||||
mapped_section = map_section_name(section_name)
|
|
||||||
if not mapped_section:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if mapped_section not in self.sections:
|
|
||||||
self.sections[mapped_section] = MemorySection(mapped_section)
|
|
||||||
self.sections[mapped_section].total_size += size
|
|
||||||
|
|
||||||
def _parse_symbols(self) -> None:
|
|
||||||
"""Parse symbols from ELF file."""
|
|
||||||
result = subprocess.run(
|
|
||||||
[self.objdump_path, "-t", str(self.elf_path)],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Track seen addresses to avoid duplicates
|
|
||||||
seen_addresses: set[str] = set()
|
|
||||||
|
|
||||||
for line in result.stdout.splitlines():
|
|
||||||
if not (symbol_info := parse_symbol_line(line)):
|
|
||||||
continue
|
|
||||||
|
|
||||||
section, name, size, address = symbol_info
|
|
||||||
|
|
||||||
# Skip duplicate symbols at the same address (e.g., C1/C2 constructors)
|
|
||||||
if address in seen_addresses or section not in self.sections:
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.sections[section].symbols.append((name, size, ""))
|
|
||||||
seen_addresses.add(address)
|
|
||||||
|
|
||||||
def _categorize_symbols(self) -> None:
|
|
||||||
"""Categorize symbols by component."""
|
|
||||||
# First, collect all unique symbol names for batch demangling
|
|
||||||
all_symbols = {
|
|
||||||
symbol_name
|
|
||||||
for section in self.sections.values()
|
|
||||||
for symbol_name, _, _ in section.symbols
|
|
||||||
}
|
|
||||||
|
|
||||||
# Batch demangle all symbols at once
|
|
||||||
self._batch_demangle_symbols(list(all_symbols))
|
|
||||||
|
|
||||||
# Now categorize with cached demangled names
|
|
||||||
for section_name, section in self.sections.items():
|
|
||||||
for symbol_name, size, _ in section.symbols:
|
|
||||||
component = self._identify_component(symbol_name)
|
|
||||||
|
|
||||||
if component not in self.components:
|
|
||||||
self.components[component] = ComponentMemory(component)
|
|
||||||
|
|
||||||
comp_mem = self.components[component]
|
|
||||||
comp_mem.symbol_count += 1
|
|
||||||
|
|
||||||
# Update the appropriate size attribute based on section
|
|
||||||
if attr_name := SECTION_TO_ATTR.get(section_name):
|
|
||||||
setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size)
|
|
||||||
|
|
||||||
# Track uncategorized symbols
|
|
||||||
if component == "other" and size > 0:
|
|
||||||
demangled = self._demangle_symbol(symbol_name)
|
|
||||||
self._uncategorized_symbols.append((symbol_name, demangled, size))
|
|
||||||
|
|
||||||
# Track ESPHome core symbols for detailed analysis
|
|
||||||
if component == _COMPONENT_CORE and size > 0:
|
|
||||||
demangled = self._demangle_symbol(symbol_name)
|
|
||||||
self._esphome_core_symbols.append((symbol_name, demangled, size))
|
|
||||||
|
|
||||||
# Track all component symbols for detailed analysis
|
|
||||||
if size > 0:
|
|
||||||
demangled = self._demangle_symbol(symbol_name)
|
|
||||||
self._component_symbols[component].append(
|
|
||||||
(symbol_name, demangled, size)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _identify_component(self, symbol_name: str) -> str:
|
|
||||||
"""Identify which component a symbol belongs to."""
|
|
||||||
# Demangle C++ names if needed
|
|
||||||
demangled = self._demangle_symbol(symbol_name)
|
|
||||||
|
|
||||||
# Check for special component classes first (before namespace pattern)
|
|
||||||
# This handles cases like esphome::ESPHomeOTAComponent which should map to ota
|
|
||||||
if _NAMESPACE_ESPHOME in demangled:
|
|
||||||
# Check for special component classes that include component name in the class
|
|
||||||
# For example: esphome::ESPHomeOTAComponent -> ota component
|
|
||||||
for component_name in get_esphome_components():
|
|
||||||
patterns = get_component_class_patterns(component_name)
|
|
||||||
if any(pattern in demangled for pattern in patterns):
|
|
||||||
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
|
|
||||||
|
|
||||||
# Check for ESPHome component namespaces
|
|
||||||
match = ESPHOME_COMPONENT_PATTERN.search(demangled)
|
|
||||||
if match:
|
|
||||||
component_name = match.group(1)
|
|
||||||
# Strip trailing underscore if present (e.g., switch_ -> switch)
|
|
||||||
component_name = component_name.rstrip("_")
|
|
||||||
|
|
||||||
# Check if this is an actual component in the components directory
|
|
||||||
if component_name in get_esphome_components():
|
|
||||||
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
|
|
||||||
# Check if this is a known external component from the config
|
|
||||||
if component_name in self.external_components:
|
|
||||||
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
|
|
||||||
# Everything else in esphome:: namespace is core
|
|
||||||
return _COMPONENT_CORE
|
|
||||||
|
|
||||||
# Check for esphome core namespace (no component namespace)
|
|
||||||
if _NAMESPACE_ESPHOME in demangled:
|
|
||||||
# If no component match found, it's core
|
|
||||||
return _COMPONENT_CORE
|
|
||||||
|
|
||||||
# Check against symbol patterns
|
|
||||||
for component, patterns in SYMBOL_PATTERNS.items():
|
|
||||||
if any(pattern in symbol_name for pattern in patterns):
|
|
||||||
return component
|
|
||||||
|
|
||||||
# Check against demangled patterns
|
|
||||||
for component, patterns in DEMANGLED_PATTERNS.items():
|
|
||||||
if any(pattern in demangled for pattern in patterns):
|
|
||||||
return component
|
|
||||||
|
|
||||||
# Special cases that need more complex logic
|
|
||||||
|
|
||||||
# Check if spi_flash vs spi_driver
|
|
||||||
if "spi_" in symbol_name or "SPI" in symbol_name:
|
|
||||||
return "spi_flash" if "spi_flash" in symbol_name else "spi_driver"
|
|
||||||
|
|
||||||
# libc special printf variants
|
|
||||||
if (
|
|
||||||
symbol_name.startswith("_")
|
|
||||||
and symbol_name[1:].replace("_r", "").replace("v", "").replace("s", "")
|
|
||||||
in _LIBC_PRINTF_SCANF_FAMILY
|
|
||||||
):
|
|
||||||
return "libc"
|
|
||||||
|
|
||||||
# Track uncategorized symbols for analysis
|
|
||||||
return "other"
|
|
||||||
|
|
||||||
def _batch_demangle_symbols(self, symbols: list[str]) -> None:
|
|
||||||
"""Batch demangle C++ symbol names for efficiency."""
|
|
||||||
if not symbols:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Try to find the appropriate c++filt for the platform
|
|
||||||
cppfilt_cmd = "c++filt"
|
|
||||||
|
|
||||||
_LOGGER.info("Demangling %d symbols", len(symbols))
|
|
||||||
_LOGGER.debug("objdump_path = %s", self.objdump_path)
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
"""Get demangled C++ symbol name from cache."""
|
|
||||||
return self._demangle_cache.get(symbol, symbol)
|
|
||||||
|
|
||||||
def _categorize_esphome_core_symbol(self, demangled: str) -> str:
|
|
||||||
"""Categorize ESPHome core symbols into subcategories."""
|
|
||||||
# Special patterns that need to be checked separately
|
|
||||||
if any(pattern in demangled for pattern in _CPP_RUNTIME_PATTERNS):
|
|
||||||
return "C++ Runtime (vtables/RTTI)"
|
|
||||||
|
|
||||||
if demangled.startswith(_NAMESPACE_STD):
|
|
||||||
return "C++ STL"
|
|
||||||
|
|
||||||
# Check against patterns from const.py
|
|
||||||
for category, patterns in CORE_SUBCATEGORY_PATTERNS.items():
|
|
||||||
if any(pattern in demangled for pattern in patterns):
|
|
||||||
return category
|
|
||||||
|
|
||||||
return "Other Core"
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
from .cli import main
|
|
||||||
|
|
||||||
main()
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
"""Main entry point for running the memory analyzer as a module."""
|
|
||||||
|
|
||||||
from .cli import main
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
"""CLI interface for memory analysis with report generation."""
|
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from . import (
|
|
||||||
_COMPONENT_API,
|
|
||||||
_COMPONENT_CORE,
|
|
||||||
_COMPONENT_PREFIX_ESPHOME,
|
|
||||||
_COMPONENT_PREFIX_EXTERNAL,
|
|
||||||
MemoryAnalyzer,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|
||||||
"""Memory analyzer with CLI-specific report generation."""
|
|
||||||
|
|
||||||
# Column width constants
|
|
||||||
COL_COMPONENT: int = 29
|
|
||||||
COL_FLASH_TEXT: int = 14
|
|
||||||
COL_FLASH_DATA: int = 14
|
|
||||||
COL_RAM_DATA: int = 12
|
|
||||||
COL_RAM_BSS: int = 12
|
|
||||||
COL_TOTAL_FLASH: int = 15
|
|
||||||
COL_TOTAL_RAM: int = 12
|
|
||||||
COL_SEPARATOR: int = 3 # " | "
|
|
||||||
|
|
||||||
# Core analysis column widths
|
|
||||||
COL_CORE_SUBCATEGORY: int = 30
|
|
||||||
COL_CORE_SIZE: int = 12
|
|
||||||
COL_CORE_COUNT: int = 6
|
|
||||||
COL_CORE_PERCENT: int = 10
|
|
||||||
|
|
||||||
# Calculate table width once at class level
|
|
||||||
TABLE_WIDTH: int = (
|
|
||||||
COL_COMPONENT
|
|
||||||
+ COL_SEPARATOR
|
|
||||||
+ COL_FLASH_TEXT
|
|
||||||
+ COL_SEPARATOR
|
|
||||||
+ COL_FLASH_DATA
|
|
||||||
+ COL_SEPARATOR
|
|
||||||
+ COL_RAM_DATA
|
|
||||||
+ COL_SEPARATOR
|
|
||||||
+ COL_RAM_BSS
|
|
||||||
+ COL_SEPARATOR
|
|
||||||
+ COL_TOTAL_FLASH
|
|
||||||
+ COL_SEPARATOR
|
|
||||||
+ COL_TOTAL_RAM
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _make_separator_line(*widths: int) -> str:
|
|
||||||
"""Create a separator line with given column widths.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
widths: Column widths to create separators for
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Separator line like "----+---------+-----"
|
|
||||||
"""
|
|
||||||
return "-+-".join("-" * width for width in widths)
|
|
||||||
|
|
||||||
# Pre-computed separator lines
|
|
||||||
MAIN_TABLE_SEPARATOR: str = _make_separator_line(
|
|
||||||
COL_COMPONENT,
|
|
||||||
COL_FLASH_TEXT,
|
|
||||||
COL_FLASH_DATA,
|
|
||||||
COL_RAM_DATA,
|
|
||||||
COL_RAM_BSS,
|
|
||||||
COL_TOTAL_FLASH,
|
|
||||||
COL_TOTAL_RAM,
|
|
||||||
)
|
|
||||||
|
|
||||||
CORE_TABLE_SEPARATOR: str = _make_separator_line(
|
|
||||||
COL_CORE_SUBCATEGORY,
|
|
||||||
COL_CORE_SIZE,
|
|
||||||
COL_CORE_COUNT,
|
|
||||||
COL_CORE_PERCENT,
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_report(self, detailed: bool = False) -> str:
|
|
||||||
"""Generate a formatted memory report."""
|
|
||||||
components = sorted(
|
|
||||||
self.components.items(), key=lambda x: x[1].flash_total, reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Calculate totals
|
|
||||||
total_flash = sum(c.flash_total for _, c in components)
|
|
||||||
total_ram = sum(c.ram_total for _, c in components)
|
|
||||||
|
|
||||||
# Build report
|
|
||||||
lines: list[str] = []
|
|
||||||
|
|
||||||
lines.append("=" * self.TABLE_WIDTH)
|
|
||||||
lines.append("Component Memory Analysis".center(self.TABLE_WIDTH))
|
|
||||||
lines.append("=" * self.TABLE_WIDTH)
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
# Main table - fixed column widths
|
|
||||||
lines.append(
|
|
||||||
f"{'Component':<{self.COL_COMPONENT}} | {'Flash (text)':>{self.COL_FLASH_TEXT}} | {'Flash (data)':>{self.COL_FLASH_DATA}} | {'RAM (data)':>{self.COL_RAM_DATA}} | {'RAM (bss)':>{self.COL_RAM_BSS}} | {'Total Flash':>{self.COL_TOTAL_FLASH}} | {'Total RAM':>{self.COL_TOTAL_RAM}}"
|
|
||||||
)
|
|
||||||
lines.append(self.MAIN_TABLE_SEPARATOR)
|
|
||||||
|
|
||||||
for name, mem in components:
|
|
||||||
if mem.flash_total > 0 or mem.ram_total > 0:
|
|
||||||
flash_rodata = mem.rodata_size + mem.data_size
|
|
||||||
lines.append(
|
|
||||||
f"{name:<{self.COL_COMPONENT}} | {mem.text_size:>{self.COL_FLASH_TEXT - 2},} B | {flash_rodata:>{self.COL_FLASH_DATA - 2},} B | "
|
|
||||||
f"{mem.data_size:>{self.COL_RAM_DATA - 2},} B | {mem.bss_size:>{self.COL_RAM_BSS - 2},} B | "
|
|
||||||
f"{mem.flash_total:>{self.COL_TOTAL_FLASH - 2},} B | {mem.ram_total:>{self.COL_TOTAL_RAM - 2},} B"
|
|
||||||
)
|
|
||||||
|
|
||||||
lines.append(self.MAIN_TABLE_SEPARATOR)
|
|
||||||
lines.append(
|
|
||||||
f"{'TOTAL':<{self.COL_COMPONENT}} | {' ':>{self.COL_FLASH_TEXT}} | {' ':>{self.COL_FLASH_DATA}} | "
|
|
||||||
f"{' ':>{self.COL_RAM_DATA}} | {' ':>{self.COL_RAM_BSS}} | "
|
|
||||||
f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Top consumers
|
|
||||||
lines.append("")
|
|
||||||
lines.append("Top Flash Consumers:")
|
|
||||||
for i, (name, mem) in enumerate(components[:25]):
|
|
||||||
if mem.flash_total > 0:
|
|
||||||
percentage = (
|
|
||||||
(mem.flash_total / total_flash * 100) if total_flash > 0 else 0
|
|
||||||
)
|
|
||||||
lines.append(
|
|
||||||
f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash"
|
|
||||||
)
|
|
||||||
|
|
||||||
lines.append("")
|
|
||||||
lines.append("Top RAM Consumers:")
|
|
||||||
ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True)
|
|
||||||
for i, (name, mem) in enumerate(ram_components[:25]):
|
|
||||||
if mem.ram_total > 0:
|
|
||||||
percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0
|
|
||||||
lines.append(
|
|
||||||
f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed 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
|
|
||||||
if self._esphome_core_symbols:
|
|
||||||
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
|
|
||||||
core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict(
|
|
||||||
list
|
|
||||||
)
|
|
||||||
|
|
||||||
for symbol, demangled, size in self._esphome_core_symbols:
|
|
||||||
# Categorize based on demangled name patterns
|
|
||||||
subcategory = self._categorize_esphome_core_symbol(demangled)
|
|
||||||
core_subcategories[subcategory].append((symbol, demangled, size))
|
|
||||||
|
|
||||||
# Sort subcategories by total size
|
|
||||||
sorted_subcategories = sorted(
|
|
||||||
[
|
|
||||||
(name, symbols, sum(s[2] for s in symbols))
|
|
||||||
for name, symbols in core_subcategories.items()
|
|
||||||
],
|
|
||||||
key=lambda x: x[2],
|
|
||||||
reverse=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
lines.append(
|
|
||||||
f"{'Subcategory':<{self.COL_CORE_SUBCATEGORY}} | {'Size':>{self.COL_CORE_SIZE}} | "
|
|
||||||
f"{'Count':>{self.COL_CORE_COUNT}} | {'% of Core':>{self.COL_CORE_PERCENT}}"
|
|
||||||
)
|
|
||||||
lines.append(self.CORE_TABLE_SEPARATOR)
|
|
||||||
|
|
||||||
core_total = sum(size for _, _, size in self._esphome_core_symbols)
|
|
||||||
|
|
||||||
for subcategory, symbols, total_size in sorted_subcategories:
|
|
||||||
percentage = (total_size / core_total * 100) if core_total > 0 else 0
|
|
||||||
lines.append(
|
|
||||||
f"{subcategory:<{self.COL_CORE_SUBCATEGORY}} | {total_size:>{self.COL_CORE_SIZE - 2},} B | "
|
|
||||||
f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Top 15 largest core symbols
|
|
||||||
lines.append("")
|
|
||||||
lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:")
|
|
||||||
sorted_core_symbols = sorted(
|
|
||||||
self._esphome_core_symbols, key=lambda x: x[2], reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]):
|
|
||||||
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
|
|
||||||
|
|
||||||
lines.append("=" * self.TABLE_WIDTH)
|
|
||||||
|
|
||||||
# Add detailed analysis for top ESPHome and external components
|
|
||||||
esphome_components = [
|
|
||||||
(name, mem)
|
|
||||||
for name, mem in components
|
|
||||||
if name.startswith(_COMPONENT_PREFIX_ESPHOME) and name != _COMPONENT_CORE
|
|
||||||
]
|
|
||||||
external_components = [
|
|
||||||
(name, mem)
|
|
||||||
for name, mem in components
|
|
||||||
if name.startswith(_COMPONENT_PREFIX_EXTERNAL)
|
|
||||||
]
|
|
||||||
|
|
||||||
top_esphome_components = sorted(
|
|
||||||
esphome_components, key=lambda x: x[1].flash_total, reverse=True
|
|
||||||
)[:30]
|
|
||||||
|
|
||||||
# Include all external components (they're usually important)
|
|
||||||
top_external_components = sorted(
|
|
||||||
external_components, key=lambda x: x[1].flash_total, reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if API component exists and ensure it's included
|
|
||||||
api_component = None
|
|
||||||
for name, mem in components:
|
|
||||||
if name == _COMPONENT_API:
|
|
||||||
api_component = (name, mem)
|
|
||||||
break
|
|
||||||
|
|
||||||
# Also include wifi_stack and other important system components if they exist
|
|
||||||
system_components_to_include = [
|
|
||||||
# Empty list - we've finished debugging symbol categorization
|
|
||||||
# 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:
|
|
||||||
components_to_analyze.append(api_component)
|
|
||||||
|
|
||||||
if components_to_analyze:
|
|
||||||
for comp_name, comp_mem in components_to_analyze:
|
|
||||||
if not (comp_symbols := self._component_symbols.get(comp_name, [])):
|
|
||||||
continue
|
|
||||||
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
|
|
||||||
sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True)
|
|
||||||
|
|
||||||
lines.append(f"Total symbols: {len(sorted_symbols)}")
|
|
||||||
lines.append(f"Total size: {comp_mem.flash_total:,} B")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
# Show all symbols > 100 bytes for better visibility
|
|
||||||
large_symbols = [
|
|
||||||
(sym, dem, size) for sym, dem, size in sorted_symbols if size > 100
|
|
||||||
]
|
|
||||||
|
|
||||||
lines.append(
|
|
||||||
f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):"
|
|
||||||
)
|
|
||||||
for i, (symbol, demangled, size) in enumerate(large_symbols):
|
|
||||||
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
|
|
||||||
|
|
||||||
lines.append("=" * self.TABLE_WIDTH)
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
|
|
||||||
"""Dump uncategorized symbols for analysis."""
|
|
||||||
# Sort by size descending
|
|
||||||
sorted_symbols = sorted(
|
|
||||||
self._uncategorized_symbols, key=lambda x: x[2], reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
lines = ["Uncategorized Symbols Analysis", "=" * 80]
|
|
||||||
lines.append(f"Total uncategorized symbols: {len(sorted_symbols)}")
|
|
||||||
lines.append(
|
|
||||||
f"Total uncategorized size: {sum(s[2] for s in sorted_symbols):,} bytes"
|
|
||||||
)
|
|
||||||
lines.append("")
|
|
||||||
lines.append(f"{'Size':>10} | {'Symbol':<60} | Demangled")
|
|
||||||
lines.append("-" * 10 + "-+-" + "-" * 60 + "-+-" + "-" * 40)
|
|
||||||
|
|
||||||
for symbol, demangled, size in sorted_symbols[:100]: # Top 100
|
|
||||||
demangled_display = (
|
|
||||||
demangled[:100] if symbol != demangled else "[not demangled]"
|
|
||||||
)
|
|
||||||
lines.append(f"{size:>10,} | {symbol[:60]:<60} | {demangled_display}")
|
|
||||||
|
|
||||||
if len(sorted_symbols) > 100:
|
|
||||||
lines.append(f"\n... and {len(sorted_symbols) - 100} more symbols")
|
|
||||||
|
|
||||||
content = "\n".join(lines)
|
|
||||||
|
|
||||||
if output_file:
|
|
||||||
with open(output_file, "w", encoding="utf-8") as f:
|
|
||||||
f.write(content)
|
|
||||||
else:
|
|
||||||
print(content)
|
|
||||||
|
|
||||||
|
|
||||||
def analyze_elf(
|
|
||||||
elf_path: str,
|
|
||||||
objdump_path: str | None = None,
|
|
||||||
readelf_path: str | None = None,
|
|
||||||
detailed: bool = False,
|
|
||||||
external_components: set[str] | None = None,
|
|
||||||
) -> str:
|
|
||||||
"""Analyze an ELF file and return a memory report."""
|
|
||||||
analyzer = MemoryAnalyzerCLI(
|
|
||||||
elf_path, objdump_path, readelf_path, external_components
|
|
||||||
)
|
|
||||||
analyzer.analyze()
|
|
||||||
return analyzer.generate_report(detailed)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""CLI entrypoint for memory analysis."""
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Usage: python -m esphome.analyze_memory <build_directory>")
|
|
||||||
print("\nAnalyze memory usage from an ESPHome build directory.")
|
|
||||||
print("The build directory should contain firmware.elf and idedata will be")
|
|
||||||
print("loaded from ~/.esphome/.internal/idedata/<device>.json")
|
|
||||||
print("\nExamples:")
|
|
||||||
print(" python -m esphome.analyze_memory ~/.esphome/build/my-device")
|
|
||||||
print(" python -m esphome.analyze_memory .esphome/build/my-device")
|
|
||||||
print(" python -m esphome.analyze_memory my-device # Short form")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
build_dir = sys.argv[1]
|
|
||||||
|
|
||||||
# Load build directory
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from esphome.platformio_api import IDEData
|
|
||||||
|
|
||||||
build_path = Path(build_dir)
|
|
||||||
|
|
||||||
# If no path separator in name, assume it's a device name
|
|
||||||
if "/" not in build_dir and not build_path.is_dir():
|
|
||||||
# Try current directory first
|
|
||||||
cwd_path = Path.cwd() / ".esphome" / "build" / build_dir
|
|
||||||
if cwd_path.is_dir():
|
|
||||||
build_path = cwd_path
|
|
||||||
print(f"Using build directory: {build_path}", file=sys.stderr)
|
|
||||||
else:
|
|
||||||
# Fall back to home directory
|
|
||||||
build_path = Path.home() / ".esphome" / "build" / build_dir
|
|
||||||
print(f"Using build directory: {build_path}", file=sys.stderr)
|
|
||||||
|
|
||||||
if not build_path.is_dir():
|
|
||||||
print(f"Error: {build_path} is not a directory", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Find firmware.elf
|
|
||||||
elf_file = None
|
|
||||||
for elf_candidate in [
|
|
||||||
build_path / "firmware.elf",
|
|
||||||
build_path / ".pioenvs" / build_path.name / "firmware.elf",
|
|
||||||
]:
|
|
||||||
if elf_candidate.exists():
|
|
||||||
elf_file = str(elf_candidate)
|
|
||||||
break
|
|
||||||
|
|
||||||
if not elf_file:
|
|
||||||
print(f"Error: firmware.elf not found in {build_dir}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Find idedata.json - check current directory first, then home
|
|
||||||
device_name = build_path.name
|
|
||||||
idedata_candidates = [
|
|
||||||
Path.cwd() / ".esphome" / "idedata" / f"{device_name}.json",
|
|
||||||
Path.home() / ".esphome" / "idedata" / f"{device_name}.json",
|
|
||||||
]
|
|
||||||
|
|
||||||
idedata = None
|
|
||||||
for idedata_path in idedata_candidates:
|
|
||||||
if not idedata_path.exists():
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
with open(idedata_path, encoding="utf-8") as f:
|
|
||||||
raw_data = json.load(f)
|
|
||||||
idedata = IDEData(raw_data)
|
|
||||||
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
|
|
||||||
break
|
|
||||||
except (json.JSONDecodeError, OSError) as e:
|
|
||||||
print(f"Warning: Failed to load idedata: {e}", file=sys.stderr)
|
|
||||||
|
|
||||||
if not idedata:
|
|
||||||
print(
|
|
||||||
f"Warning: idedata not found (searched {idedata_candidates[0]} and {idedata_candidates[1]})",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
|
|
||||||
analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata)
|
|
||||||
analyzer.analyze()
|
|
||||||
report = analyzer.generate_report()
|
|
||||||
print(report)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,121 +0,0 @@
|
|||||||
"""Helper functions for memory analysis."""
|
|
||||||
|
|
||||||
from functools import cache
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from .const import SECTION_MAPPING
|
|
||||||
|
|
||||||
# Import namespace constant from parent module
|
|
||||||
# Note: This would create a circular import if done at module level,
|
|
||||||
# so we'll define it locally here as well
|
|
||||||
_NAMESPACE_ESPHOME = "esphome::"
|
|
||||||
|
|
||||||
|
|
||||||
# Get the list of actual ESPHome components by scanning the components directory
|
|
||||||
@cache
|
|
||||||
def get_esphome_components():
|
|
||||||
"""Get set of actual ESPHome components from the components directory."""
|
|
||||||
# Find the components directory relative to this file
|
|
||||||
# Go up two levels from analyze_memory/helpers.py to esphome/
|
|
||||||
current_dir = Path(__file__).parent.parent
|
|
||||||
components_dir = current_dir / "components"
|
|
||||||
|
|
||||||
if not components_dir.exists() or not components_dir.is_dir():
|
|
||||||
return frozenset()
|
|
||||||
|
|
||||||
return frozenset(
|
|
||||||
item.name
|
|
||||||
for item in components_dir.iterdir()
|
|
||||||
if item.is_dir()
|
|
||||||
and not item.name.startswith(".")
|
|
||||||
and not item.name.startswith("__")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@cache
|
|
||||||
def get_component_class_patterns(component_name: str) -> list[str]:
|
|
||||||
"""Generate component class name patterns for symbol matching.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
component_name: The component name (e.g., "ota", "wifi", "api")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of pattern strings to match against demangled symbols
|
|
||||||
"""
|
|
||||||
component_upper = component_name.upper()
|
|
||||||
component_camel = component_name.replace("_", "").title()
|
|
||||||
return [
|
|
||||||
f"{_NAMESPACE_ESPHOME}{component_upper}Component", # e.g., esphome::OTAComponent
|
|
||||||
f"{_NAMESPACE_ESPHOME}ESPHome{component_upper}Component", # e.g., esphome::ESPHomeOTAComponent
|
|
||||||
f"{_NAMESPACE_ESPHOME}{component_camel}Component", # e.g., esphome::OtaComponent
|
|
||||||
f"{_NAMESPACE_ESPHOME}ESPHome{component_camel}Component", # e.g., esphome::ESPHomeOtaComponent
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def map_section_name(raw_section: str) -> str | None:
|
|
||||||
"""Map raw section name to standard section.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
raw_section: Raw section name from ELF file (e.g., ".iram0.text", ".rodata.str1.1")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Standard section name (".text", ".rodata", ".data", ".bss") or None
|
|
||||||
"""
|
|
||||||
for standard_section, patterns in SECTION_MAPPING.items():
|
|
||||||
if any(pattern in raw_section for pattern in patterns):
|
|
||||||
return standard_section
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None:
|
|
||||||
"""Parse a single symbol line from objdump output.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
line: Line from objdump -t output
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (section, name, size, address) or None if not a valid symbol.
|
|
||||||
Format: address l/g w/d F/O section size name
|
|
||||||
Example: 40084870 l F .iram0.text 00000000 _xt_user_exc
|
|
||||||
"""
|
|
||||||
parts = line.split()
|
|
||||||
if len(parts) < 5:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Validate and extract address
|
|
||||||
address = parts[0]
|
|
||||||
int(address, 16)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Look for F (function) or O (object) flag
|
|
||||||
if "F" not in parts and "O" not in parts:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Find section, size, and name
|
|
||||||
for i, part in enumerate(parts):
|
|
||||||
if not part.startswith("."):
|
|
||||||
continue
|
|
||||||
|
|
||||||
section = map_section_name(part)
|
|
||||||
if not section:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Need at least size field after section
|
|
||||||
if i + 1 >= len(parts):
|
|
||||||
break
|
|
||||||
|
|
||||||
try:
|
|
||||||
size = int(parts[i + 1], 16)
|
|
||||||
except ValueError:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Need symbol name and non-zero size
|
|
||||||
if i + 2 >= len(parts) or size == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
name = " ".join(parts[i + 2 :])
|
|
||||||
return (section, name, size, address)
|
|
||||||
|
|
||||||
return None
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ from esphome.cpp_types import ( # noqa: F401
|
|||||||
EntityBase,
|
EntityBase,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
ESPTime,
|
ESPTime,
|
||||||
FixedVector,
|
|
||||||
GPIOPin,
|
GPIOPin,
|
||||||
InternalGPIOPin,
|
InternalGPIOPin,
|
||||||
JsonObject,
|
JsonObject,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
#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"
|
||||||
@@ -36,9 +34,6 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState 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;
|
||||||
this->state_callback_.call();
|
this->state_callback_.call();
|
||||||
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
|
|
||||||
ControllerRegistry::notify_alarm_control_panel_update(this);
|
|
||||||
#endif
|
|
||||||
if (state == ACP_STATE_TRIGGERED) {
|
if (state == ACP_STATE_TRIGGERED) {
|
||||||
this->triggered_callback_.call();
|
this->triggered_callback_.call();
|
||||||
} else if (state == ACP_STATE_ARMING) {
|
} else if (state == ACP_STATE_ARMING) {
|
||||||
|
|||||||
@@ -89,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()) {
|
||||||
@@ -109,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()) {
|
||||||
@@ -129,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()) {
|
||||||
@@ -149,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_;
|
||||||
@@ -159,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_;
|
||||||
@@ -169,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_;
|
||||||
@@ -178,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ uint32_t Animation::get_animation_frame_count() const { return this->animation_f
|
|||||||
int Animation::get_current_frame() const { return this->current_frame_; }
|
int Animation::get_current_frame() const { return this->current_frame_; }
|
||||||
void Animation::next_frame() {
|
void Animation::next_frame() {
|
||||||
this->current_frame_++;
|
this->current_frame_++;
|
||||||
if (loop_count_ && static_cast<uint32_t>(this->current_frame_) == loop_end_frame_ &&
|
if (loop_count_ && this->current_frame_ == loop_end_frame_ &&
|
||||||
(this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
|
(this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
|
||||||
this->current_frame_ = loop_start_frame_;
|
this->current_frame_ = loop_start_frame_;
|
||||||
this->loop_current_iteration_++;
|
this->loop_current_iteration_++;
|
||||||
}
|
}
|
||||||
if (static_cast<uint32_t>(this->current_frame_) >= animation_frame_count_) {
|
if (this->current_frame_ >= animation_frame_count_) {
|
||||||
this->loop_current_iteration_ = 1;
|
this->loop_current_iteration_ = 1;
|
||||||
this->current_frame_ = 0;
|
this->current_frame_ = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import esphome.config_validation as cv
|
|||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ACTION,
|
CONF_ACTION,
|
||||||
CONF_ACTIONS,
|
CONF_ACTIONS,
|
||||||
CONF_CAPTURE_RESPONSE,
|
|
||||||
CONF_DATA,
|
CONF_DATA,
|
||||||
CONF_DATA_TEMPLATE,
|
CONF_DATA_TEMPLATE,
|
||||||
CONF_EVENT,
|
CONF_EVENT,
|
||||||
@@ -18,50 +17,30 @@ from esphome.const import (
|
|||||||
CONF_MAX_CONNECTIONS,
|
CONF_MAX_CONNECTIONS,
|
||||||
CONF_ON_CLIENT_CONNECTED,
|
CONF_ON_CLIENT_CONNECTED,
|
||||||
CONF_ON_CLIENT_DISCONNECTED,
|
CONF_ON_CLIENT_DISCONNECTED,
|
||||||
CONF_ON_ERROR,
|
|
||||||
CONF_ON_SUCCESS,
|
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_REBOOT_TIMEOUT,
|
CONF_REBOOT_TIMEOUT,
|
||||||
CONF_RESPONSE_TEMPLATE,
|
|
||||||
CONF_SERVICE,
|
CONF_SERVICE,
|
||||||
CONF_SERVICES,
|
CONF_SERVICES,
|
||||||
CONF_TAG,
|
CONF_TAG,
|
||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
CONF_VARIABLES,
|
CONF_VARIABLES,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||||
from esphome.cpp_generator import TemplateArgsType
|
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "api"
|
DOMAIN = "api"
|
||||||
DEPENDENCIES = ["network"]
|
DEPENDENCIES = ["network"]
|
||||||
|
AUTO_LOAD = ["socket"]
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
|
|
||||||
|
|
||||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
|
|
||||||
"""Conditionally auto-load json only when capture_response is used."""
|
|
||||||
base = ["socket"]
|
|
||||||
|
|
||||||
# Check if any homeassistant.action/homeassistant.service has capture_response: true
|
|
||||||
# This flag is set during config validation in _validate_response_config
|
|
||||||
if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
|
|
||||||
return base + ["json"]
|
|
||||||
|
|
||||||
return base
|
|
||||||
|
|
||||||
|
|
||||||
api_ns = cg.esphome_ns.namespace("api")
|
api_ns = cg.esphome_ns.namespace("api")
|
||||||
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
|
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
|
||||||
HomeAssistantServiceCallAction = api_ns.class_(
|
HomeAssistantServiceCallAction = api_ns.class_(
|
||||||
"HomeAssistantServiceCallAction", automation.Action
|
"HomeAssistantServiceCallAction", automation.Action
|
||||||
)
|
)
|
||||||
ActionResponse = api_ns.class_("ActionResponse")
|
|
||||||
HomeAssistantActionResponseTrigger = api_ns.class_(
|
|
||||||
"HomeAssistantActionResponseTrigger", automation.Trigger
|
|
||||||
)
|
|
||||||
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
|
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
|
||||||
|
|
||||||
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
|
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
|
||||||
@@ -71,12 +50,10 @@ SERVICE_ARG_NATIVE_TYPES = {
|
|||||||
"int": cg.int32,
|
"int": cg.int32,
|
||||||
"float": float,
|
"float": float,
|
||||||
"string": cg.std_string,
|
"string": cg.std_string,
|
||||||
"bool[]": cg.FixedVector.template(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(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"
|
||||||
@@ -157,17 +134,6 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
|
||||||
"""Register socket needs for API component."""
|
|
||||||
from esphome.components import socket
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -227,7 +193,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
|
||||||
@@ -236,7 +201,6 @@ 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),
|
||||||
_validate_api_config,
|
_validate_api_config,
|
||||||
_consume_api_sockets,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -245,9 +209,6 @@ 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]:
|
if config[CONF_PASSWORD]:
|
||||||
cg.add_define("USE_API_PASSWORD")
|
cg.add_define("USE_API_PASSWORD")
|
||||||
@@ -264,10 +225,6 @@ async def to_code(config):
|
|||||||
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_SERVICES")
|
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")
|
||||||
|
|
||||||
@@ -275,8 +232,6 @@ async def to_code(config):
|
|||||||
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:
|
||||||
template_args = []
|
template_args = []
|
||||||
func_args = []
|
func_args = []
|
||||||
@@ -290,10 +245,8 @@ async def to_code(config):
|
|||||||
trigger = cg.new_Pvariable(
|
trigger = cg.new_Pvariable(
|
||||||
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
|
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
|
||||||
)
|
)
|
||||||
triggers.append(trigger)
|
cg.add(var.register_user_service(trigger))
|
||||||
await automation.build_automation(trigger, func_args, conf)
|
await automation.build_automation(trigger, func_args, conf)
|
||||||
# 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")
|
||||||
@@ -335,29 +288,6 @@ async def to_code(config):
|
|||||||
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
|
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
|
||||||
|
|
||||||
|
|
||||||
def _validate_response_config(config: ConfigType) -> ConfigType:
|
|
||||||
# Validate dependencies:
|
|
||||||
# - response_template requires capture_response: true
|
|
||||||
# - capture_response: true requires on_success
|
|
||||||
if CONF_RESPONSE_TEMPLATE in config and not config[CONF_CAPTURE_RESPONSE]:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.",
|
|
||||||
path=[CONF_RESPONSE_TEMPLATE],
|
|
||||||
)
|
|
||||||
|
|
||||||
if config[CONF_CAPTURE_RESPONSE] and CONF_ON_SUCCESS not in config:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"`{CONF_CAPTURE_RESPONSE}: true` requires `{CONF_ON_SUCCESS}` to be set.",
|
|
||||||
path=[CONF_CAPTURE_RESPONSE],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Track if any action uses capture_response for AUTO_LOAD
|
|
||||||
if config[CONF_CAPTURE_RESPONSE]:
|
|
||||||
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -373,15 +303,10 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
|
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
|
||||||
{cv.string: cv.returning_lambda}
|
{cv.string: cv.returning_lambda}
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
|
|
||||||
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
|
|
||||||
cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True),
|
|
||||||
cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
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),
|
||||||
_validate_response_config,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -395,67 +320,21 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
|||||||
HomeAssistantServiceCallAction,
|
HomeAssistantServiceCallAction,
|
||||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
||||||
)
|
)
|
||||||
async def homeassistant_service_to_code(
|
async def homeassistant_service_to_code(config, action_id, template_arg, args):
|
||||||
config: ConfigType,
|
|
||||||
action_id: ID,
|
|
||||||
template_arg: cg.TemplateArguments,
|
|
||||||
args: TemplateArgsType,
|
|
||||||
):
|
|
||||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||||
serv = await cg.get_variable(config[CONF_ID])
|
serv = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||||
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
||||||
cg.add(var.set_service(templ))
|
cg.add(var.set_service(templ))
|
||||||
|
|
||||||
# Initialize FixedVectors with exact sizes from config
|
|
||||||
cg.add(var.init_data(len(config[CONF_DATA])))
|
|
||||||
for key, value in config[CONF_DATA].items():
|
for key, value in config[CONF_DATA].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_data(key, templ))
|
cg.add(var.add_data(key, templ))
|
||||||
|
|
||||||
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
|
||||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_data_template(key, templ))
|
cg.add(var.add_data_template(key, templ))
|
||||||
|
|
||||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
|
||||||
for key, value in config[CONF_VARIABLES].items():
|
for key, value in config[CONF_VARIABLES].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_variable(key, templ))
|
cg.add(var.add_variable(key, templ))
|
||||||
|
|
||||||
if on_error := config.get(CONF_ON_ERROR):
|
|
||||||
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
|
||||||
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS")
|
|
||||||
cg.add(var.set_wants_status())
|
|
||||||
await automation.build_automation(
|
|
||||||
var.get_error_trigger(),
|
|
||||||
[(cg.std_string, "error"), *args],
|
|
||||||
on_error,
|
|
||||||
)
|
|
||||||
|
|
||||||
if on_success := config.get(CONF_ON_SUCCESS):
|
|
||||||
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
|
||||||
cg.add(var.set_wants_status())
|
|
||||||
if config[CONF_CAPTURE_RESPONSE]:
|
|
||||||
cg.add(var.set_wants_response())
|
|
||||||
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON")
|
|
||||||
await automation.build_automation(
|
|
||||||
var.get_success_trigger_with_response(),
|
|
||||||
[(cg.JsonObjectConst, "response"), *args],
|
|
||||||
on_success,
|
|
||||||
)
|
|
||||||
|
|
||||||
if response_template := config.get(CONF_RESPONSE_TEMPLATE):
|
|
||||||
templ = await cg.templatable(response_template, args, cg.std_string)
|
|
||||||
cg.add(var.set_response_template(templ))
|
|
||||||
|
|
||||||
else:
|
|
||||||
await automation.build_automation(
|
|
||||||
var.get_success_trigger(),
|
|
||||||
args,
|
|
||||||
on_success,
|
|
||||||
)
|
|
||||||
|
|
||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
@@ -491,23 +370,15 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
|||||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||||
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
||||||
cg.add(var.set_service(templ))
|
cg.add(var.set_service(templ))
|
||||||
|
|
||||||
# Initialize FixedVectors with exact sizes from config
|
|
||||||
cg.add(var.init_data(len(config[CONF_DATA])))
|
|
||||||
for key, value in config[CONF_DATA].items():
|
for key, value in config[CONF_DATA].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_data(key, templ))
|
cg.add(var.add_data(key, templ))
|
||||||
|
|
||||||
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
|
||||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_data_template(key, templ))
|
cg.add(var.add_data_template(key, templ))
|
||||||
|
|
||||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
|
||||||
for key, value in config[CONF_VARIABLES].items():
|
for key, value in config[CONF_VARIABLES].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_variable(key, templ))
|
cg.add(var.add_variable(key, templ))
|
||||||
|
|
||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
@@ -530,8 +401,6 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
|
|||||||
serv = await cg.get_variable(config[CONF_ID])
|
serv = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||||
cg.add(var.set_service("esphome.tag_scanned"))
|
cg.add(var.set_service("esphome.tag_scanned"))
|
||||||
# Initialize FixedVector with exact size (1 data field)
|
|
||||||
cg.add(var.init_data(1))
|
|
||||||
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
|
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
|
||||||
cg.add(var.add_data("tag_id", templ))
|
cg.add(var.add_data("tag_id", templ))
|
||||||
return var
|
return var
|
||||||
|
|||||||
@@ -425,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
|
||||||
@@ -506,7 +506,7 @@ message ListEntitiesLightResponse {
|
|||||||
string name = 3;
|
string name = 3;
|
||||||
reserved 4; // Deprecated: was string unique_id
|
reserved 4; // Deprecated: was string unique_id
|
||||||
|
|
||||||
repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
|
repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set<light::ColorMode>"];
|
||||||
// next four supports_* are for legacy clients, newer clients should use color modes
|
// next four supports_* are for legacy clients, newer clients should use color modes
|
||||||
// Deprecated in API version 1.6
|
// Deprecated in API version 1.6
|
||||||
bool legacy_supports_brightness = 5 [deprecated=true];
|
bool legacy_supports_brightness = 5 [deprecated=true];
|
||||||
@@ -776,26 +776,10 @@ message HomeassistantActionRequest {
|
|||||||
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
||||||
|
|
||||||
string service = 1;
|
string service = 1;
|
||||||
repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true];
|
repeated HomeassistantServiceMap data = 2;
|
||||||
repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true];
|
repeated HomeassistantServiceMap data_template = 3;
|
||||||
repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true];
|
repeated HomeassistantServiceMap variables = 4;
|
||||||
bool is_event = 5;
|
bool is_event = 5;
|
||||||
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
|
|
||||||
bool wants_response = 7 [(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 HomeassistantActionResponse {
|
|
||||||
option (id) = 130;
|
|
||||||
option (source) = SOURCE_CLIENT;
|
|
||||||
option (no_delay) = true;
|
|
||||||
option (ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES";
|
|
||||||
|
|
||||||
uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
|
|
||||||
bool success = 2; // Whether the service call succeeded
|
|
||||||
string error_message = 3; // Error message if success = false
|
|
||||||
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== IMPORT HOME ASSISTANT STATES ====================
|
// ==================== IMPORT HOME ASSISTANT STATES ====================
|
||||||
@@ -866,7 +850,7 @@ message ListEntitiesServicesResponse {
|
|||||||
|
|
||||||
string name = 1;
|
string name = 1;
|
||||||
fixed32 key = 2;
|
fixed32 key = 2;
|
||||||
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
|
repeated ListEntitiesServicesArgument args = 3;
|
||||||
}
|
}
|
||||||
message ExecuteServiceArgument {
|
message ExecuteServiceArgument {
|
||||||
option (ifdef) = "USE_API_SERVICES";
|
option (ifdef) = "USE_API_SERVICES";
|
||||||
@@ -876,10 +860,10 @@ message ExecuteServiceArgument {
|
|||||||
string string_ = 4;
|
string string_ = 4;
|
||||||
// ESPHome 1.14 (api v1.3) make int a signed value
|
// ESPHome 1.14 (api v1.3) make int a signed value
|
||||||
sint32 int_ = 5;
|
sint32 int_ = 5;
|
||||||
repeated bool bool_array = 6 [packed=false, (fixed_vector) = true];
|
repeated bool bool_array = 6 [packed=false];
|
||||||
repeated sint32 int_array = 7 [packed=false, (fixed_vector) = true];
|
repeated sint32 int_array = 7 [packed=false];
|
||||||
repeated float float_array = 8 [packed=false, (fixed_vector) = true];
|
repeated float float_array = 8 [packed=false];
|
||||||
repeated string string_array = 9 [(fixed_vector) = true];
|
repeated string string_array = 9;
|
||||||
}
|
}
|
||||||
message ExecuteServiceRequest {
|
message ExecuteServiceRequest {
|
||||||
option (id) = 42;
|
option (id) = 42;
|
||||||
@@ -888,7 +872,7 @@ message ExecuteServiceRequest {
|
|||||||
option (ifdef) = "USE_API_SERVICES";
|
option (ifdef) = "USE_API_SERVICES";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
|
repeated ExecuteServiceArgument args = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== CAMERA ====================
|
// ==================== CAMERA ====================
|
||||||
@@ -987,9 +971,9 @@ message ListEntitiesClimateResponse {
|
|||||||
string name = 3;
|
string name = 3;
|
||||||
reserved 4; // Deprecated: was string unique_id
|
reserved 4; // Deprecated: was string unique_id
|
||||||
|
|
||||||
bool supports_current_temperature = 5; // Deprecated: use feature_flags
|
bool supports_current_temperature = 5;
|
||||||
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
|
bool supports_two_point_target_temperature = 6;
|
||||||
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;
|
||||||
@@ -997,22 +981,21 @@ message ListEntitiesClimateResponse {
|
|||||||
// is if CLIMATE_PRESET_AWAY exists is supported_presets
|
// is if CLIMATE_PRESET_AWAY exists is supported_presets
|
||||||
// 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;
|
||||||
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;
|
||||||
float visual_current_temperature_step = 21;
|
float visual_current_temperature_step = 21;
|
||||||
bool supports_current_humidity = 22; // Deprecated: use feature_flags
|
bool supports_current_humidity = 22;
|
||||||
bool supports_target_humidity = 23; // Deprecated: use feature_flags
|
bool supports_target_humidity = 23;
|
||||||
float visual_min_humidity = 24;
|
float visual_min_humidity = 24;
|
||||||
float visual_max_humidity = 25;
|
float visual_max_humidity = 25;
|
||||||
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
|
||||||
uint32 feature_flags = 27;
|
|
||||||
}
|
}
|
||||||
message ClimateStateResponse {
|
message ClimateStateResponse {
|
||||||
option (id) = 47;
|
option (id) = 47;
|
||||||
@@ -1143,7 +1126,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"];
|
||||||
@@ -1520,7 +1503,7 @@ message BluetoothGATTCharacteristic {
|
|||||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
|
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
|
||||||
uint32 handle = 2;
|
uint32 handle = 2;
|
||||||
uint32 properties = 3;
|
uint32 properties = 3;
|
||||||
repeated BluetoothGATTDescriptor descriptors = 4 [(fixed_vector) = true];
|
repeated BluetoothGATTDescriptor descriptors = 4;
|
||||||
|
|
||||||
// New field for efficient UUID (v1.12+)
|
// New field for efficient UUID (v1.12+)
|
||||||
// Only one of uuid or short_uuid will be set.
|
// Only one of uuid or short_uuid will be set.
|
||||||
@@ -1532,7 +1515,7 @@ message BluetoothGATTCharacteristic {
|
|||||||
message BluetoothGATTService {
|
message BluetoothGATTService {
|
||||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
|
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
|
||||||
uint32 handle = 2;
|
uint32 handle = 2;
|
||||||
repeated BluetoothGATTCharacteristic characteristics = 3 [(fixed_vector) = true];
|
repeated BluetoothGATTCharacteristic characteristics = 3;
|
||||||
|
|
||||||
// New field for efficient UUID (v1.12+)
|
// New field for efficient UUID (v1.12+)
|
||||||
// Only one of uuid or short_uuid will be set.
|
// Only one of uuid or short_uuid will be set.
|
||||||
@@ -2147,7 +2130,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 {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
#endif
|
#endif
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
|
#include <utility>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <utility>
|
|
||||||
#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/entity_base.h"
|
#include "esphome/core/entity_base.h"
|
||||||
@@ -27,9 +27,6 @@
|
|||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
|
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_CLIMATE
|
|
||||||
#include "esphome/components/climate/climate_mode.h"
|
|
||||||
#endif
|
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
#include "esphome/components/voice_assistant/voice_assistant.h"
|
#include "esphome/components/voice_assistant/voice_assistant.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -410,8 +407,8 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
|
|||||||
}
|
}
|
||||||
if (traits.supports_direction())
|
if (traits.supports_direction())
|
||||||
msg.direction = static_cast<enums::FanDirection>(fan->direction);
|
msg.direction = static_cast<enums::FanDirection>(fan->direction);
|
||||||
if (traits.supports_preset_modes() && fan->has_preset_mode())
|
if (traits.supports_preset_modes())
|
||||||
msg.set_preset_mode(StringRef(fan->get_preset_mode()));
|
msg.set_preset_mode(StringRef(fan->preset_mode));
|
||||||
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
@@ -423,7 +420,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
|
|||||||
msg.supports_speed = traits.supports_speed();
|
msg.supports_speed = traits.supports_speed();
|
||||||
msg.supports_direction = traits.supports_direction();
|
msg.supports_direction = traits.supports_direction();
|
||||||
msg.supported_speed_count = traits.supported_speed_count();
|
msg.supported_speed_count = traits.supported_speed_count();
|
||||||
msg.supported_preset_modes = &traits.supported_preset_modes();
|
msg.supported_preset_modes = &traits.supported_preset_modes_for_api_();
|
||||||
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||||
}
|
}
|
||||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
|
void APIConnection::fan_command(const FanCommandRequest &msg) {
|
||||||
@@ -453,6 +450,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
|
|||||||
bool is_single) {
|
bool is_single) {
|
||||||
auto *light = static_cast<light::LightState *>(entity);
|
auto *light = static_cast<light::LightState *>(entity);
|
||||||
LightStateResponse resp;
|
LightStateResponse resp;
|
||||||
|
auto traits = light->get_traits();
|
||||||
auto values = light->remote_values;
|
auto values = light->remote_values;
|
||||||
auto color_mode = values.get_color_mode();
|
auto color_mode = values.get_color_mode();
|
||||||
resp.state = values.is_on();
|
resp.state = values.is_on();
|
||||||
@@ -476,8 +474,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
|||||||
auto *light = static_cast<light::LightState *>(entity);
|
auto *light = static_cast<light::LightState *>(entity);
|
||||||
ListEntitiesLightResponse msg;
|
ListEntitiesLightResponse msg;
|
||||||
auto traits = light->get_traits();
|
auto traits = light->get_traits();
|
||||||
// Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
|
msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
|
||||||
msg.supported_color_modes = &traits.get_supported_color_modes();
|
|
||||||
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
|
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
|
||||||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
|
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
|
||||||
msg.min_mireds = traits.get_min_mireds();
|
msg.min_mireds = traits.get_min_mireds();
|
||||||
@@ -486,7 +483,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
|||||||
if (light->supports_effects()) {
|
if (light->supports_effects()) {
|
||||||
msg.effects.emplace_back("None");
|
msg.effects.emplace_back("None");
|
||||||
for (auto *effect : light->get_effects()) {
|
for (auto *effect : light->get_effects()) {
|
||||||
msg.effects.emplace_back(effect->get_name());
|
msg.effects.push_back(effect->get_name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
|
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||||
@@ -626,10 +623,9 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
|
|||||||
auto traits = climate->get_traits();
|
auto traits = climate->get_traits();
|
||||||
resp.mode = static_cast<enums::ClimateMode>(climate->mode);
|
resp.mode = static_cast<enums::ClimateMode>(climate->mode);
|
||||||
resp.action = static_cast<enums::ClimateAction>(climate->action);
|
resp.action = static_cast<enums::ClimateAction>(climate->action);
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE))
|
if (traits.get_supports_current_temperature())
|
||||||
resp.current_temperature = climate->current_temperature;
|
resp.current_temperature = climate->current_temperature;
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
if (traits.get_supports_two_point_target_temperature()) {
|
||||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
|
||||||
resp.target_temperature_low = climate->target_temperature_low;
|
resp.target_temperature_low = climate->target_temperature_low;
|
||||||
resp.target_temperature_high = climate->target_temperature_high;
|
resp.target_temperature_high = climate->target_temperature_high;
|
||||||
} else {
|
} else {
|
||||||
@@ -637,20 +633,20 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
|
|||||||
}
|
}
|
||||||
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
|
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
|
||||||
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
|
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
|
||||||
if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) {
|
if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) {
|
||||||
resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode()));
|
resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value()));
|
||||||
}
|
}
|
||||||
if (traits.get_supports_presets() && climate->preset.has_value()) {
|
if (traits.get_supports_presets() && climate->preset.has_value()) {
|
||||||
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
|
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) {
|
if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) {
|
||||||
resp.set_custom_preset(StringRef(climate->get_custom_preset()));
|
resp.set_custom_preset(StringRef(climate->custom_preset.value()));
|
||||||
}
|
}
|
||||||
if (traits.get_supports_swing_modes())
|
if (traits.get_supports_swing_modes())
|
||||||
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
|
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY))
|
if (traits.get_supports_current_humidity())
|
||||||
resp.current_humidity = climate->current_humidity;
|
resp.current_humidity = climate->current_humidity;
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY))
|
if (traits.get_supports_target_humidity())
|
||||||
resp.target_humidity = climate->target_humidity;
|
resp.target_humidity = climate->target_humidity;
|
||||||
return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size,
|
return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||||
is_single);
|
is_single);
|
||||||
@@ -660,27 +656,23 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
|||||||
auto *climate = static_cast<climate::Climate *>(entity);
|
auto *climate = static_cast<climate::Climate *>(entity);
|
||||||
ListEntitiesClimateResponse msg;
|
ListEntitiesClimateResponse msg;
|
||||||
auto traits = climate->get_traits();
|
auto traits = climate->get_traits();
|
||||||
// Flags set for backward compatibility, deprecated in 2025.11.0
|
msg.supports_current_temperature = traits.get_supports_current_temperature();
|
||||||
msg.supports_current_temperature = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
msg.supports_current_humidity = traits.get_supports_current_humidity();
|
||||||
msg.supports_current_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
|
||||||
msg.supports_two_point_target_temperature = traits.has_feature_flags(
|
msg.supports_target_humidity = traits.get_supports_target_humidity();
|
||||||
climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
|
msg.supported_modes = &traits.get_supported_modes_for_api_();
|
||||||
msg.supports_target_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
|
|
||||||
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
|
|
||||||
// Current feature flags and other supported parameters
|
|
||||||
msg.feature_flags = traits.get_feature_flags();
|
|
||||||
msg.supported_modes = &traits.get_supported_modes();
|
|
||||||
msg.visual_min_temperature = traits.get_visual_min_temperature();
|
msg.visual_min_temperature = traits.get_visual_min_temperature();
|
||||||
msg.visual_max_temperature = traits.get_visual_max_temperature();
|
msg.visual_max_temperature = traits.get_visual_max_temperature();
|
||||||
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
|
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
|
||||||
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
|
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
|
||||||
msg.visual_min_humidity = traits.get_visual_min_humidity();
|
msg.visual_min_humidity = traits.get_visual_min_humidity();
|
||||||
msg.visual_max_humidity = traits.get_visual_max_humidity();
|
msg.visual_max_humidity = traits.get_visual_max_humidity();
|
||||||
msg.supported_fan_modes = &traits.get_supported_fan_modes();
|
msg.supports_action = traits.get_supports_action();
|
||||||
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes();
|
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
|
||||||
msg.supported_presets = &traits.get_supported_presets();
|
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
|
||||||
msg.supported_custom_presets = &traits.get_supported_custom_presets();
|
msg.supported_presets = &traits.get_supported_presets_for_api_();
|
||||||
msg.supported_swing_modes = &traits.get_supported_swing_modes();
|
msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_();
|
||||||
|
msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_();
|
||||||
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
|
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||||
is_single);
|
is_single);
|
||||||
}
|
}
|
||||||
@@ -877,7 +869,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
|
|||||||
bool is_single) {
|
bool is_single) {
|
||||||
auto *select = static_cast<select::Select *>(entity);
|
auto *select = static_cast<select::Select *>(entity);
|
||||||
SelectStateResponse resp;
|
SelectStateResponse resp;
|
||||||
resp.set_state(StringRef(select->current_option()));
|
resp.set_state(StringRef(select->state));
|
||||||
resp.missing_state = !select->has_state();
|
resp.missing_state = !select->has_state();
|
||||||
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||||
}
|
}
|
||||||
@@ -1082,8 +1074,13 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
|||||||
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
||||||
#ifdef USE_TIME_TIMEZONE
|
#ifdef USE_TIME_TIMEZONE
|
||||||
if (value.timezone_len > 0) {
|
if (value.timezone_len > 0) {
|
||||||
homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast<const char *>(value.timezone),
|
const std::string ¤t_tz = homeassistant::global_homeassistant_time->get_timezone();
|
||||||
value.timezone_len);
|
// Compare without allocating a string
|
||||||
|
if (current_tz.length() != value.timezone_len ||
|
||||||
|
memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) {
|
||||||
|
homeassistant::global_homeassistant_time->set_timezone(
|
||||||
|
std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -1294,11 +1291,11 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
void APIConnection::send_event(event::Event *event, const char *event_type) {
|
void APIConnection::send_event(event::Event *event, const std::string &event_type) {
|
||||||
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
|
this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
|
||||||
EventResponse::ESTIMATED_SIZE);
|
EventResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
|
uint16_t APIConnection::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) {
|
||||||
EventResponse resp;
|
EventResponse resp;
|
||||||
resp.set_event_type(StringRef(event_type));
|
resp.set_event_type(StringRef(event_type));
|
||||||
@@ -1310,7 +1307,8 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
|
|||||||
auto *event = static_cast<event::Event *>(entity);
|
auto *event = static_cast<event::Event *>(entity);
|
||||||
ListEntitiesEventResponse msg;
|
ListEntitiesEventResponse msg;
|
||||||
msg.set_device_class(event->get_device_class_ref());
|
msg.set_device_class(event->get_device_class_ref());
|
||||||
msg.event_types = &event->get_event_types();
|
for (const auto &event_type : event->get_event_types())
|
||||||
|
msg.event_types.push_back(event_type);
|
||||||
return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size,
|
return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||||
is_single);
|
is_single);
|
||||||
}
|
}
|
||||||
@@ -1408,7 +1406,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
|||||||
|
|
||||||
HelloResponse resp;
|
HelloResponse resp;
|
||||||
resp.api_version_major = 1;
|
resp.api_version_major = 1;
|
||||||
resp.api_version_minor = 13;
|
resp.api_version_minor = 12;
|
||||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||||
resp.set_server_info(ESPHOME_VERSION_REF);
|
resp.set_server_info(ESPHOME_VERSION_REF);
|
||||||
resp.set_name(StringRef(App.get_name()));
|
resp.set_name(StringRef(App.get_name()));
|
||||||
@@ -1467,8 +1465,6 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
|||||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Beken");
|
static constexpr auto MANUFACTURER = StringRef::from_lit("Beken");
|
||||||
#elif defined(USE_LN882X)
|
#elif defined(USE_LN882X)
|
||||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning");
|
static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning");
|
||||||
#elif defined(USE_NRF52)
|
|
||||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor");
|
|
||||||
#elif defined(USE_RTL87XX)
|
#elif defined(USE_RTL87XX)
|
||||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek");
|
static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek");
|
||||||
#elif defined(USE_HOST)
|
#elif defined(USE_HOST)
|
||||||
@@ -1553,33 +1549,13 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
if (msg.response_data_len > 0) {
|
|
||||||
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data,
|
|
||||||
msg.response_data_len);
|
|
||||||
} else
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
|
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
|
||||||
NoiseEncryptionSetKeyResponse resp;
|
NoiseEncryptionSetKeyResponse resp;
|
||||||
resp.success = false;
|
resp.success = false;
|
||||||
|
|
||||||
psk_t psk{};
|
psk_t psk{};
|
||||||
if (msg.key.empty()) {
|
if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
|
||||||
if (this->parent_->clear_noise_psk(true)) {
|
|
||||||
resp.success = true;
|
|
||||||
} else {
|
|
||||||
ESP_LOGW(TAG, "Failed to clear encryption key");
|
|
||||||
}
|
|
||||||
} else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
|
|
||||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||||
} else if (!this->parent_->save_noise_psk(psk, true)) {
|
} else if (!this->parent_->save_noise_psk(psk, true)) {
|
||||||
ESP_LOGW(TAG, "Failed to save encryption key");
|
ESP_LOGW(TAG, "Failed to save encryption key");
|
||||||
@@ -1650,7 +1626,9 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
|
|||||||
// O(n) but optimized for RAM and not performance.
|
// O(n) but optimized for RAM and not performance.
|
||||||
for (auto &item : items) {
|
for (auto &item : items) {
|
||||||
if (item.entity == entity && item.message_type == message_type) {
|
if (item.entity == entity && item.message_type == message_type) {
|
||||||
// Replace with new creator
|
// Clean up old creator before replacing
|
||||||
|
item.creator.cleanup(message_type);
|
||||||
|
// Move assign the new creator
|
||||||
item.creator = std::move(creator);
|
item.creator = std::move(creator);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1820,7 +1798,7 @@ void APIConnection::process_batch_() {
|
|||||||
|
|
||||||
// Handle remaining items more efficiently
|
// Handle remaining items more efficiently
|
||||||
if (items_processed < this->deferred_batch_.size()) {
|
if (items_processed < this->deferred_batch_.size()) {
|
||||||
// Remove processed items from the beginning
|
// Remove processed items from the beginning with proper cleanup
|
||||||
this->deferred_batch_.remove_front(items_processed);
|
this->deferred_batch_.remove_front(items_processed);
|
||||||
// Reschedule for remaining items
|
// Reschedule for remaining items
|
||||||
this->schedule_batch_();
|
this->schedule_batch_();
|
||||||
@@ -1833,10 +1811,10 @@ void APIConnection::process_batch_() {
|
|||||||
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single, uint8_t message_type) const {
|
bool is_single, uint8_t message_type) const {
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
// Special case: EventResponse uses const char * pointer
|
// Special case: EventResponse uses string pointer
|
||||||
if (message_type == EventResponse::MESSAGE_TYPE) {
|
if (message_type == EventResponse::MESSAGE_TYPE) {
|
||||||
auto *e = static_cast<event::Event *>(entity);
|
auto *e = static_cast<event::Event *>(entity);
|
||||||
return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single);
|
return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -129,10 +129,7 @@ class APIConnection final : public APIServerConnection {
|
|||||||
return;
|
return;
|
||||||
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
|
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
#endif
|
||||||
void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
|
|
||||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||||
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||||
@@ -177,7 +174,7 @@ class APIConnection final : public APIServerConnection {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
void send_event(event::Event *event, const char *event_type);
|
void send_event(event::Event *event, const std::string &event_type);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_UPDATE
|
#ifdef USE_UPDATE
|
||||||
@@ -450,7 +447,7 @@ class APIConnection final : public APIServerConnection {
|
|||||||
bool is_single);
|
bool is_single);
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
static uint16_t try_send_event_response(event::Event *event, const char *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
|
||||||
@@ -508,8 +505,10 @@ class APIConnection final : public APIServerConnection {
|
|||||||
// Constructor for function pointer
|
// Constructor for function pointer
|
||||||
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
|
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
|
||||||
|
|
||||||
// Constructor for const char * (Event types - no allocation needed)
|
// Constructor for string state capture
|
||||||
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
|
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
|
// Delete copy operations - MessageCreator should only be moved
|
||||||
MessageCreator(const MessageCreator &other) = delete;
|
MessageCreator(const MessageCreator &other) = delete;
|
||||||
@@ -521,6 +520,8 @@ class APIConnection final : public APIServerConnection {
|
|||||||
// Move assignment
|
// Move assignment
|
||||||
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
||||||
if (this != &other) {
|
if (this != &other) {
|
||||||
|
// IMPORTANT: Caller must ensure cleanup() was called if this contains a string!
|
||||||
|
// In our usage, this happens in add_item() deduplication and vector::erase()
|
||||||
data_ = other.data_;
|
data_ = other.data_;
|
||||||
other.data_.function_ptr = nullptr;
|
other.data_.function_ptr = nullptr;
|
||||||
}
|
}
|
||||||
@@ -531,10 +532,20 @@ class APIConnection final : public APIServerConnection {
|
|||||||
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 - same as before
|
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -554,24 +565,42 @@ class APIConnection final : public APIServerConnection {
|
|||||||
std::vector<BatchItem> items;
|
std::vector<BatchItem> items;
|
||||||
uint32_t batch_start_time{0};
|
uint32_t batch_start_time{0};
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Helper to cleanup items from the beginning
|
||||||
|
void cleanup_items_(size_t count) {
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
items[i].creator.cleanup(items[i].message_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
DeferredBatch() {
|
DeferredBatch() {
|
||||||
// Pre-allocate capacity for typical batch sizes to avoid reallocation
|
// Pre-allocate capacity for typical batch sizes to avoid reallocation
|
||||||
items.reserve(8);
|
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(); }
|
||||||
@@ -650,30 +679,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)) {
|
||||||
@@ -691,27 +711,6 @@ 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, std::move(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, std::move(creator), message_type, estimated_size);
|
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
|
||||||
|
|||||||
@@ -18,17 +18,6 @@ namespace esphome::api {
|
|||||||
// uncomment to log raw packets
|
// uncomment to log raw packets
|
||||||
//#define HELPER_LOG_PACKETS
|
//#define HELPER_LOG_PACKETS
|
||||||
|
|
||||||
// Maximum message size limits to prevent OOM on constrained devices
|
|
||||||
// Handshake messages are limited to a small size for security
|
|
||||||
static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128;
|
|
||||||
|
|
||||||
// Data message limits vary by platform based on available memory
|
|
||||||
#ifdef USE_ESP8266
|
|
||||||
static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
|
|
||||||
#else
|
|
||||||
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Forward declaration
|
// Forward declaration
|
||||||
struct ClientInfo;
|
struct ClientInfo;
|
||||||
|
|
||||||
|
|||||||
@@ -132,16 +132,26 @@ APIError APINoiseFrameHelper::loop() {
|
|||||||
return APIFrameHelper::loop();
|
return APIFrameHelper::loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Read a packet into the rx_buf_.
|
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
|
||||||
*
|
*
|
||||||
* @return APIError::OK if a full packet is in rx_buf_
|
* @param frame: The struct to hold the frame information in.
|
||||||
|
* msg_start: points to the start of the payload - this pointer is only valid until the next
|
||||||
|
* try_receive_raw_ call
|
||||||
|
*
|
||||||
|
* @return 0 if a full packet is in rx_buf_
|
||||||
|
* @return -1 if error, check errno.
|
||||||
*
|
*
|
||||||
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
|
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
|
||||||
* errno ENOMEM: Not enough memory for reading packet.
|
* errno ENOMEM: Not enough memory for reading packet.
|
||||||
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
||||||
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
|
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
|
||||||
*/
|
*/
|
||||||
APIError APINoiseFrameHelper::try_read_frame_() {
|
APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
|
||||||
|
if (frame == nullptr) {
|
||||||
|
HELPER_LOG("Bad argument for try_read_frame_");
|
||||||
|
return APIError::BAD_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
// read header
|
// read header
|
||||||
if (rx_header_buf_len_ < 3) {
|
if (rx_header_buf_len_ < 3) {
|
||||||
// no header information yet
|
// no header information yet
|
||||||
@@ -168,17 +178,16 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
|||||||
// read body
|
// read body
|
||||||
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
|
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
|
||||||
|
|
||||||
// Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
|
if (state_ != State::DATA && msg_size > 128) {
|
||||||
uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
|
// for handshake message only permit up to 128 bytes
|
||||||
if (msg_size > limit) {
|
|
||||||
state_ = State::FAILED;
|
state_ = State::FAILED;
|
||||||
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
|
HELPER_LOG("Bad packet len for handshake: %d", msg_size);
|
||||||
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
|
return APIError::BAD_HANDSHAKE_PACKET_LEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserve space for body
|
// reserve space for body
|
||||||
if (this->rx_buf_.size() != msg_size) {
|
if (rx_buf_.size() != msg_size) {
|
||||||
this->rx_buf_.resize(msg_size);
|
rx_buf_.resize(msg_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rx_buf_len_ < msg_size) {
|
if (rx_buf_len_ < msg_size) {
|
||||||
@@ -196,12 +205,12 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_PACKET_RECEIVED(this->rx_buf_);
|
LOG_PACKET_RECEIVED(rx_buf_);
|
||||||
|
*frame = std::move(rx_buf_);
|
||||||
// Clear state for next frame (rx_buf_ still contains data for caller)
|
// consume msg
|
||||||
this->rx_buf_len_ = 0;
|
rx_buf_ = {};
|
||||||
this->rx_header_buf_len_ = 0;
|
rx_buf_len_ = 0;
|
||||||
|
rx_header_buf_len_ = 0;
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,17 +232,18 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
}
|
}
|
||||||
if (state_ == State::CLIENT_HELLO) {
|
if (state_ == State::CLIENT_HELLO) {
|
||||||
// waiting for client hello
|
// waiting for client hello
|
||||||
aerr = this->try_read_frame_();
|
std::vector<uint8_t> frame;
|
||||||
|
aerr = try_read_frame_(&frame);
|
||||||
if (aerr != APIError::OK) {
|
if (aerr != APIError::OK) {
|
||||||
return handle_handshake_frame_error_(aerr);
|
return handle_handshake_frame_error_(aerr);
|
||||||
}
|
}
|
||||||
// ignore contents, may be used in future for flags
|
// ignore contents, may be used in future for flags
|
||||||
// Resize for: existing prologue + 2 size bytes + frame data
|
// Resize for: existing prologue + 2 size bytes + frame data
|
||||||
size_t old_size = this->prologue_.size();
|
size_t old_size = prologue_.size();
|
||||||
this->prologue_.resize(old_size + 2 + this->rx_buf_.size());
|
prologue_.resize(old_size + 2 + frame.size());
|
||||||
this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8);
|
prologue_[old_size] = (uint8_t) (frame.size() >> 8);
|
||||||
this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size();
|
prologue_[old_size + 1] = (uint8_t) frame.size();
|
||||||
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size());
|
std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size());
|
||||||
|
|
||||||
state_ = State::SERVER_HELLO;
|
state_ = State::SERVER_HELLO;
|
||||||
}
|
}
|
||||||
@@ -242,6 +252,7 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
const std::string &name = App.get_name();
|
const std::string &name = App.get_name();
|
||||||
const std::string &mac = get_mac_address();
|
const std::string &mac = get_mac_address();
|
||||||
|
|
||||||
|
std::vector<uint8_t> msg;
|
||||||
// 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 mac_len = mac.size() + 1; // including null terminator
|
||||||
@@ -249,17 +260,17 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
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;
|
||||||
|
|
||||||
auto msg = std::make_unique<uint8_t[]>(total_size);
|
msg.resize(total_size);
|
||||||
|
|
||||||
// chosen proto
|
// chosen proto
|
||||||
msg[0] = 0x01;
|
msg[0] = 0x01;
|
||||||
|
|
||||||
// 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.data() + 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.c_str(), mac_len);
|
std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len);
|
||||||
|
|
||||||
aerr = write_frame_(msg.get(), total_size);
|
aerr = write_frame_(msg.data(), msg.size());
|
||||||
if (aerr != APIError::OK)
|
if (aerr != APIError::OK)
|
||||||
return aerr;
|
return aerr;
|
||||||
|
|
||||||
@@ -274,23 +285,24 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
int action = noise_handshakestate_get_action(handshake_);
|
int action = noise_handshakestate_get_action(handshake_);
|
||||||
if (action == NOISE_ACTION_READ_MESSAGE) {
|
if (action == NOISE_ACTION_READ_MESSAGE) {
|
||||||
// waiting for handshake msg
|
// waiting for handshake msg
|
||||||
aerr = this->try_read_frame_();
|
std::vector<uint8_t> frame;
|
||||||
|
aerr = try_read_frame_(&frame);
|
||||||
if (aerr != APIError::OK) {
|
if (aerr != APIError::OK) {
|
||||||
return handle_handshake_frame_error_(aerr);
|
return handle_handshake_frame_error_(aerr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->rx_buf_.empty()) {
|
if (frame.empty()) {
|
||||||
send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
|
send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
|
||||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||||
} else if (this->rx_buf_[0] != 0x00) {
|
} else if (frame[0] != 0x00) {
|
||||||
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
|
HELPER_LOG("Bad handshake error byte: %u", frame[0]);
|
||||||
send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
|
send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
|
||||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
NoiseBuffer mbuf;
|
NoiseBuffer mbuf;
|
||||||
noise_buffer_init(mbuf);
|
noise_buffer_init(mbuf);
|
||||||
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
|
noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1);
|
||||||
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
|
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
// Special handling for MAC failure
|
// Special handling for MAC failure
|
||||||
@@ -338,62 +350,64 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
|
|||||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||||
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
||||||
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
||||||
size_t data_size = reason_len + 1;
|
std::vector<uint8_t> data;
|
||||||
auto data = std::make_unique<uint8_t[]>(data_size);
|
data.resize(reason_len + 1);
|
||||||
data[0] = 0x01; // failure
|
data[0] = 0x01; // failure
|
||||||
|
|
||||||
// Copy error message from PROGMEM
|
// Copy error message from PROGMEM
|
||||||
if (reason_len > 0) {
|
if (reason_len > 0) {
|
||||||
memcpy_P(data.get() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
memcpy_P(data.data() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// Normal memory access
|
// Normal memory access
|
||||||
const char *reason_str = LOG_STR_ARG(reason);
|
const char *reason_str = LOG_STR_ARG(reason);
|
||||||
size_t reason_len = strlen(reason_str);
|
size_t reason_len = strlen(reason_str);
|
||||||
size_t data_size = reason_len + 1;
|
std::vector<uint8_t> data;
|
||||||
auto data = std::make_unique<uint8_t[]>(data_size);
|
data.resize(reason_len + 1);
|
||||||
data[0] = 0x01; // failure
|
data[0] = 0x01; // failure
|
||||||
|
|
||||||
// Copy error message in bulk
|
// Copy error message in bulk
|
||||||
if (reason_len > 0) {
|
if (reason_len > 0) {
|
||||||
std::memcpy(data.get() + 1, reason_str, reason_len);
|
std::memcpy(data.data() + 1, reason_str, reason_len);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// temporarily remove failed state
|
// temporarily remove failed state
|
||||||
auto orig_state = state_;
|
auto orig_state = state_;
|
||||||
state_ = State::EXPLICIT_REJECT;
|
state_ = State::EXPLICIT_REJECT;
|
||||||
write_frame_(data.get(), data_size);
|
write_frame_(data.data(), data.size());
|
||||||
state_ = orig_state;
|
state_ = orig_state;
|
||||||
}
|
}
|
||||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||||
APIError aerr = this->state_action_();
|
int err;
|
||||||
|
APIError aerr;
|
||||||
|
aerr = state_action_();
|
||||||
if (aerr != APIError::OK) {
|
if (aerr != APIError::OK) {
|
||||||
return aerr;
|
return aerr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->state_ != State::DATA) {
|
if (state_ != State::DATA) {
|
||||||
return APIError::WOULD_BLOCK;
|
return APIError::WOULD_BLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
aerr = this->try_read_frame_();
|
std::vector<uint8_t> frame;
|
||||||
|
aerr = try_read_frame_(&frame);
|
||||||
if (aerr != APIError::OK)
|
if (aerr != APIError::OK)
|
||||||
return aerr;
|
return aerr;
|
||||||
|
|
||||||
NoiseBuffer mbuf;
|
NoiseBuffer mbuf;
|
||||||
noise_buffer_init(mbuf);
|
noise_buffer_init(mbuf);
|
||||||
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size());
|
noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size());
|
||||||
int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf);
|
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
|
||||||
APIError decrypt_err =
|
APIError decrypt_err =
|
||||||
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
|
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
|
||||||
if (decrypt_err != APIError::OK) {
|
if (decrypt_err != APIError::OK)
|
||||||
return decrypt_err;
|
return decrypt_err;
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t msg_size = mbuf.size;
|
uint16_t msg_size = mbuf.size;
|
||||||
uint8_t *msg_data = this->rx_buf_.data();
|
uint8_t *msg_data = frame.data();
|
||||||
if (msg_size < 4) {
|
if (msg_size < 4) {
|
||||||
this->state_ = State::FAILED;
|
state_ = State::FAILED;
|
||||||
HELPER_LOG("Bad data packet: size %d too short", msg_size);
|
HELPER_LOG("Bad data packet: size %d too short", msg_size);
|
||||||
return APIError::BAD_DATA_PACKET;
|
return APIError::BAD_DATA_PACKET;
|
||||||
}
|
}
|
||||||
@@ -401,12 +415,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|||||||
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
|
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
|
||||||
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
|
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
|
||||||
if (data_len > msg_size - 4) {
|
if (data_len > msg_size - 4) {
|
||||||
this->state_ = State::FAILED;
|
state_ = State::FAILED;
|
||||||
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
|
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
|
||||||
return APIError::BAD_DATA_PACKET;
|
return APIError::BAD_DATA_PACKET;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer->container = std::move(this->rx_buf_);
|
buffer->container = std::move(frame);
|
||||||
buffer->data_offset = 4;
|
buffer->data_offset = 4;
|
||||||
buffer->data_len = data_len;
|
buffer->data_len = data_len;
|
||||||
buffer->type = type;
|
buffer->type = type;
|
||||||
@@ -434,7 +448,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
|||||||
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
|
||||||
|
|
||||||
this->reusable_iovs_.clear();
|
this->reusable_iovs_.clear();
|
||||||
this->reusable_iovs_.reserve(packets.size());
|
this->reusable_iovs_.reserve(packets.size());
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
APIError state_action_();
|
APIError state_action_();
|
||||||
APIError try_read_frame_();
|
APIError try_read_frame_(std::vector<uint8_t> *frame);
|
||||||
APIError write_frame_(const uint8_t *data, uint16_t len);
|
APIError write_frame_(const uint8_t *data, uint16_t len);
|
||||||
APIError init_handshake_();
|
APIError init_handshake_();
|
||||||
APIError check_handshake_finished_();
|
APIError check_handshake_finished_();
|
||||||
|
|||||||
@@ -47,13 +47,21 @@ APIError APIPlaintextFrameHelper::loop() {
|
|||||||
return APIFrameHelper::loop();
|
return APIFrameHelper::loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Read a packet into the rx_buf_.
|
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
|
||||||
|
*
|
||||||
|
* @param frame: The struct to hold the frame information in.
|
||||||
|
* msg: store the parsed frame in that struct
|
||||||
*
|
*
|
||||||
* @return See APIError
|
* @return See APIError
|
||||||
*
|
*
|
||||||
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
||||||
*/
|
*/
|
||||||
APIError APIPlaintextFrameHelper::try_read_frame_() {
|
APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
|
||||||
|
if (frame == nullptr) {
|
||||||
|
HELPER_LOG("Bad argument for try_read_frame_");
|
||||||
|
return APIError::BAD_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
// read header
|
// read header
|
||||||
while (!rx_header_parsed_) {
|
while (!rx_header_parsed_) {
|
||||||
// Now that we know when the socket is ready, we can read up to 3 bytes
|
// Now that we know when the socket is ready, we can read up to 3 bytes
|
||||||
@@ -115,10 +123,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
|
if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
|
||||||
state_ = State::FAILED;
|
state_ = State::FAILED;
|
||||||
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
|
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
|
||||||
MAX_MESSAGE_SIZE);
|
std::numeric_limits<uint16_t>::max());
|
||||||
return APIError::BAD_DATA_PACKET;
|
return APIError::BAD_DATA_PACKET;
|
||||||
}
|
}
|
||||||
rx_header_parsed_len_ = msg_size_varint->as_uint16();
|
rx_header_parsed_len_ = msg_size_varint->as_uint16();
|
||||||
@@ -142,9 +150,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
|
|||||||
}
|
}
|
||||||
// header reading done
|
// header reading done
|
||||||
|
|
||||||
// Reserve space for body
|
// reserve space for body
|
||||||
if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
|
if (rx_buf_.size() != rx_header_parsed_len_) {
|
||||||
this->rx_buf_.resize(this->rx_header_parsed_len_);
|
rx_buf_.resize(rx_header_parsed_len_);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rx_buf_len_ < rx_header_parsed_len_) {
|
if (rx_buf_len_ < rx_header_parsed_len_) {
|
||||||
@@ -162,22 +170,24 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_PACKET_RECEIVED(this->rx_buf_);
|
LOG_PACKET_RECEIVED(rx_buf_);
|
||||||
|
*frame = std::move(rx_buf_);
|
||||||
// Clear state for next frame (rx_buf_ still contains data for caller)
|
// consume msg
|
||||||
this->rx_buf_len_ = 0;
|
rx_buf_ = {};
|
||||||
this->rx_header_buf_pos_ = 0;
|
rx_buf_len_ = 0;
|
||||||
this->rx_header_parsed_ = false;
|
rx_header_buf_pos_ = 0;
|
||||||
|
rx_header_parsed_ = false;
|
||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||||
if (this->state_ != State::DATA) {
|
APIError aerr;
|
||||||
|
|
||||||
|
if (state_ != State::DATA) {
|
||||||
return APIError::WOULD_BLOCK;
|
return APIError::WOULD_BLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
APIError aerr = this->try_read_frame_();
|
std::vector<uint8_t> frame;
|
||||||
|
aerr = try_read_frame_(&frame);
|
||||||
if (aerr != APIError::OK) {
|
if (aerr != APIError::OK) {
|
||||||
if (aerr == APIError::BAD_INDICATOR) {
|
if (aerr == APIError::BAD_INDICATOR) {
|
||||||
// Make sure to tell the remote that we don't
|
// Make sure to tell the remote that we don't
|
||||||
@@ -210,10 +220,10 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
|||||||
return aerr;
|
return aerr;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer->container = std::move(this->rx_buf_);
|
buffer->container = std::move(frame);
|
||||||
buffer->data_offset = 0;
|
buffer->data_offset = 0;
|
||||||
buffer->data_len = this->rx_header_parsed_len_;
|
buffer->data_len = rx_header_parsed_len_;
|
||||||
buffer->type = this->rx_header_parsed_type_;
|
buffer->type = 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) {
|
||||||
@@ -230,7 +240,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
|
|||||||
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
|
||||||
|
|
||||||
this->reusable_iovs_.clear();
|
this->reusable_iovs_.clear();
|
||||||
this->reusable_iovs_.reserve(packets.size());
|
this->reusable_iovs_.reserve(packets.size());
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
|||||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
APIError try_read_frame_();
|
APIError try_read_frame_(std::vector<uint8_t> *frame);
|
||||||
|
|
||||||
// Group 2-byte aligned types
|
// Group 2-byte aligned types
|
||||||
uint16_t rx_header_parsed_type_ = 0;
|
uint16_t rx_header_parsed_type_ = 0;
|
||||||
|
|||||||
@@ -64,20 +64,4 @@ extend google.protobuf.FieldOptions {
|
|||||||
// This is typically done through methods returning const T& or special accessor
|
// This is typically done through methods returning const T& or special accessor
|
||||||
// methods like get_options() or supported_modes_for_api_().
|
// methods like get_options() or supported_modes_for_api_().
|
||||||
optional string container_pointer = 50001;
|
optional string container_pointer = 50001;
|
||||||
|
|
||||||
// fixed_vector: Use FixedVector instead of std::vector for repeated fields
|
|
||||||
// When set, the repeated field will use FixedVector<T> which requires calling
|
|
||||||
// init(size) before adding elements. This eliminates std::vector template overhead
|
|
||||||
// and is ideal when the exact size is known before populating the array.
|
|
||||||
optional bool fixed_vector = 50013 [default=false];
|
|
||||||
|
|
||||||
// container_pointer_no_template: Use a non-template container type for repeated fields
|
|
||||||
// Similar to container_pointer, but for containers that don't take template parameters.
|
|
||||||
// The container type is used as-is without appending element type.
|
|
||||||
// The container must have:
|
|
||||||
// - begin() and end() methods returning iterators
|
|
||||||
// - empty() method
|
|
||||||
// Example: [(container_pointer_no_template) = "light::ColorModeMask"]
|
|
||||||
// generates: const light::ColorModeMask *supported_color_modes{};
|
|
||||||
optional string container_pointer_no_template = 50014;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -355,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_string(10, this->icon_ref_);
|
buffer.encode_string(10, this->icon_ref_);
|
||||||
#endif
|
#endif
|
||||||
buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category));
|
buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category));
|
||||||
for (const char *it : *this->supported_preset_modes) {
|
for (const auto &it : *this->supported_preset_modes) {
|
||||||
buffer.encode_string(12, it, strlen(it), true);
|
buffer.encode_string(12, it, true);
|
||||||
}
|
}
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
buffer.encode_uint32(13, this->device_id);
|
buffer.encode_uint32(13, this->device_id);
|
||||||
@@ -376,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const {
|
|||||||
#endif
|
#endif
|
||||||
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
|
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
|
||||||
if (!this->supported_preset_modes->empty()) {
|
if (!this->supported_preset_modes->empty()) {
|
||||||
for (const char *it : *this->supported_preset_modes) {
|
for (const auto &it : *this->supported_preset_modes) {
|
||||||
size.add_length_force(1, strlen(it));
|
size.add_length_force(1, it.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
@@ -884,15 +884,6 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_message(4, it, true);
|
buffer.encode_message(4, it, true);
|
||||||
}
|
}
|
||||||
buffer.encode_bool(5, this->is_event);
|
buffer.encode_bool(5, this->is_event);
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
buffer.encode_uint32(6, this->call_id);
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
buffer.encode_bool(7, this->wants_response);
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
buffer.encode_string(8, this->response_template);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
|
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
|
||||||
size.add_length(1, this->service_ref_.size());
|
size.add_length(1, this->service_ref_.size());
|
||||||
@@ -900,48 +891,6 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
|
|||||||
size.add_repeated_message(1, this->data_template);
|
size.add_repeated_message(1, this->data_template);
|
||||||
size.add_repeated_message(1, this->variables);
|
size.add_repeated_message(1, this->variables);
|
||||||
size.add_bool(1, this->is_event);
|
size.add_bool(1, this->is_event);
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
size.add_uint32(1, this->call_id);
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
size.add_bool(1, this->wants_response);
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
size.add_length(1, this->response_template.size());
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
|
||||||
switch (field_id) {
|
|
||||||
case 1:
|
|
||||||
this->call_id = value.as_uint32();
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
this->success = value.as_bool();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
|
||||||
switch (field_id) {
|
|
||||||
case 3:
|
|
||||||
this->error_message = value.as_string();
|
|
||||||
break;
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
case 4: {
|
|
||||||
// Use raw data directly to avoid allocation
|
|
||||||
this->response_data = value.data();
|
|
||||||
this->response_data_len = value.size();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
@@ -1064,17 +1013,6 @@ bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
|
|
||||||
uint32_t count_bool_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 6);
|
|
||||||
this->bool_array.init(count_bool_array);
|
|
||||||
uint32_t count_int_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 7);
|
|
||||||
this->int_array.init(count_int_array);
|
|
||||||
uint32_t count_float_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 8);
|
|
||||||
this->float_array.init(count_float_array);
|
|
||||||
uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9);
|
|
||||||
this->string_array.init(count_string_array);
|
|
||||||
ProtoDecodableMessage::decode(buffer, length);
|
|
||||||
}
|
|
||||||
bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 2:
|
case 2:
|
||||||
@@ -1096,11 +1034,6 @@ bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
|
|
||||||
uint32_t count_args = ProtoDecodableMessage::count_repeated_field(buffer, length, 2);
|
|
||||||
this->args.init(count_args);
|
|
||||||
ProtoDecodableMessage::decode(buffer, length);
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
|
void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
|
||||||
@@ -1179,14 +1112,14 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
for (const auto &it : *this->supported_swing_modes) {
|
for (const auto &it : *this->supported_swing_modes) {
|
||||||
buffer.encode_uint32(14, static_cast<uint32_t>(it), true);
|
buffer.encode_uint32(14, static_cast<uint32_t>(it), true);
|
||||||
}
|
}
|
||||||
for (const char *it : *this->supported_custom_fan_modes) {
|
for (const auto &it : *this->supported_custom_fan_modes) {
|
||||||
buffer.encode_string(15, it, strlen(it), true);
|
buffer.encode_string(15, it, true);
|
||||||
}
|
}
|
||||||
for (const auto &it : *this->supported_presets) {
|
for (const auto &it : *this->supported_presets) {
|
||||||
buffer.encode_uint32(16, static_cast<uint32_t>(it), true);
|
buffer.encode_uint32(16, static_cast<uint32_t>(it), true);
|
||||||
}
|
}
|
||||||
for (const char *it : *this->supported_custom_presets) {
|
for (const auto &it : *this->supported_custom_presets) {
|
||||||
buffer.encode_string(17, it, strlen(it), true);
|
buffer.encode_string(17, it, true);
|
||||||
}
|
}
|
||||||
buffer.encode_bool(18, this->disabled_by_default);
|
buffer.encode_bool(18, this->disabled_by_default);
|
||||||
#ifdef USE_ENTITY_ICON
|
#ifdef USE_ENTITY_ICON
|
||||||
@@ -1201,7 +1134,6 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
buffer.encode_uint32(26, this->device_id);
|
buffer.encode_uint32(26, this->device_id);
|
||||||
#endif
|
#endif
|
||||||
buffer.encode_uint32(27, this->feature_flags);
|
|
||||||
}
|
}
|
||||||
void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
|
void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
|
||||||
size.add_length(1, this->object_id_ref_.size());
|
size.add_length(1, this->object_id_ref_.size());
|
||||||
@@ -1229,8 +1161,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this->supported_custom_fan_modes->empty()) {
|
if (!this->supported_custom_fan_modes->empty()) {
|
||||||
for (const char *it : *this->supported_custom_fan_modes) {
|
for (const auto &it : *this->supported_custom_fan_modes) {
|
||||||
size.add_length_force(1, strlen(it));
|
size.add_length_force(1, it.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this->supported_presets->empty()) {
|
if (!this->supported_presets->empty()) {
|
||||||
@@ -1239,8 +1171,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this->supported_custom_presets->empty()) {
|
if (!this->supported_custom_presets->empty()) {
|
||||||
for (const char *it : *this->supported_custom_presets) {
|
for (const auto &it : *this->supported_custom_presets) {
|
||||||
size.add_length_force(2, strlen(it));
|
size.add_length_force(2, it.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
size.add_bool(2, this->disabled_by_default);
|
size.add_bool(2, this->disabled_by_default);
|
||||||
@@ -1256,7 +1188,6 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
|
|||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
size.add_uint32(2, this->device_id);
|
size.add_uint32(2, this->device_id);
|
||||||
#endif
|
#endif
|
||||||
size.add_uint32(2, this->feature_flags);
|
|
||||||
}
|
}
|
||||||
void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
|
void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_fixed32(1, this->key);
|
buffer.encode_fixed32(1, this->key);
|
||||||
@@ -1475,8 +1406,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
#ifdef USE_ENTITY_ICON
|
#ifdef USE_ENTITY_ICON
|
||||||
buffer.encode_string(5, this->icon_ref_);
|
buffer.encode_string(5, this->icon_ref_);
|
||||||
#endif
|
#endif
|
||||||
for (const char *it : *this->options) {
|
for (const auto &it : *this->options) {
|
||||||
buffer.encode_string(6, it, strlen(it), true);
|
buffer.encode_string(6, it, true);
|
||||||
}
|
}
|
||||||
buffer.encode_bool(7, this->disabled_by_default);
|
buffer.encode_bool(7, this->disabled_by_default);
|
||||||
buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
|
buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
|
||||||
@@ -1492,8 +1423,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const {
|
|||||||
size.add_length(1, this->icon_ref_.size());
|
size.add_length(1, this->icon_ref_.size());
|
||||||
#endif
|
#endif
|
||||||
if (!this->options->empty()) {
|
if (!this->options->empty()) {
|
||||||
for (const char *it : *this->options) {
|
for (const auto &it : *this->options) {
|
||||||
size.add_length_force(1, strlen(it));
|
size.add_length_force(1, it.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
size.add_bool(1, this->disabled_by_default);
|
size.add_bool(1, this->disabled_by_default);
|
||||||
@@ -2877,8 +2808,8 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_bool(6, this->disabled_by_default);
|
buffer.encode_bool(6, this->disabled_by_default);
|
||||||
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
|
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
|
||||||
buffer.encode_string(8, this->device_class_ref_);
|
buffer.encode_string(8, this->device_class_ref_);
|
||||||
for (const char *it : *this->event_types) {
|
for (auto &it : this->event_types) {
|
||||||
buffer.encode_string(9, it, strlen(it), true);
|
buffer.encode_string(9, it, true);
|
||||||
}
|
}
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
buffer.encode_uint32(10, this->device_id);
|
buffer.encode_uint32(10, this->device_id);
|
||||||
@@ -2894,9 +2825,9 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const {
|
|||||||
size.add_bool(1, this->disabled_by_default);
|
size.add_bool(1, this->disabled_by_default);
|
||||||
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
|
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
|
||||||
size.add_length(1, this->device_class_ref_.size());
|
size.add_length(1, this->device_class_ref_.size());
|
||||||
if (!this->event_types->empty()) {
|
if (!this->event_types.empty()) {
|
||||||
for (const char *it : *this->event_types) {
|
for (const auto &it : this->event_types) {
|
||||||
size.add_length_force(1, strlen(it));
|
size.add_length_force(1, it.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
|
|||||||
@@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
|
|||||||
bool supports_speed{false};
|
bool supports_speed{false};
|
||||||
bool supports_direction{false};
|
bool supports_direction{false};
|
||||||
int32_t supported_speed_count{0};
|
int32_t supported_speed_count{0};
|
||||||
const std::vector<const char *> *supported_preset_modes{};
|
const std::set<std::string> *supported_preset_modes{};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
|
|||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "list_entities_light_response"; }
|
const char *message_name() const override { return "list_entities_light_response"; }
|
||||||
#endif
|
#endif
|
||||||
const light::ColorModeMask *supported_color_modes{};
|
const std::set<light::ColorMode> *supported_color_modes{};
|
||||||
float min_mireds{0.0f};
|
float min_mireds{0.0f};
|
||||||
float max_mireds{0.0f};
|
float max_mireds{0.0f};
|
||||||
std::vector<std::string> effects{};
|
std::vector<std::string> effects{};
|
||||||
@@ -1104,25 +1104,16 @@ class HomeassistantServiceMap final : public ProtoMessage {
|
|||||||
class HomeassistantActionRequest final : public ProtoMessage {
|
class HomeassistantActionRequest final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 35;
|
static constexpr uint8_t MESSAGE_TYPE = 35;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 128;
|
static constexpr uint8_t ESTIMATED_SIZE = 113;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "homeassistant_action_request"; }
|
const char *message_name() const override { return "homeassistant_action_request"; }
|
||||||
#endif
|
#endif
|
||||||
StringRef service_ref_{};
|
StringRef service_ref_{};
|
||||||
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
|
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
|
||||||
FixedVector<HomeassistantServiceMap> data{};
|
std::vector<HomeassistantServiceMap> data{};
|
||||||
FixedVector<HomeassistantServiceMap> data_template{};
|
std::vector<HomeassistantServiceMap> data_template{};
|
||||||
FixedVector<HomeassistantServiceMap> variables{};
|
std::vector<HomeassistantServiceMap> variables{};
|
||||||
bool is_event{false};
|
bool is_event{false};
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
uint32_t call_id{0};
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
bool wants_response{false};
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
std::string response_template{};
|
|
||||||
#endif
|
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@@ -1132,30 +1123,6 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
|||||||
protected:
|
protected:
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
class HomeassistantActionResponse final : public ProtoDecodableMessage {
|
|
||||||
public:
|
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 130;
|
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 34;
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
|
||||||
const char *message_name() const override { return "homeassistant_action_response"; }
|
|
||||||
#endif
|
|
||||||
uint32_t call_id{0};
|
|
||||||
bool success{false};
|
|
||||||
std::string error_message{};
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
const uint8_t *response_data{nullptr};
|
|
||||||
uint16_t response_data_len{0};
|
|
||||||
#endif
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
|
||||||
void dump_to(std::string &out) const override;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
|
||||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
|
||||||
};
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
|
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
@@ -1263,7 +1230,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
|
|||||||
StringRef name_ref_{};
|
StringRef name_ref_{};
|
||||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||||
uint32_t key{0};
|
uint32_t key{0};
|
||||||
FixedVector<ListEntitiesServicesArgument> args{};
|
std::vector<ListEntitiesServicesArgument> args{};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@@ -1279,11 +1246,10 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
|
|||||||
float float_{0.0f};
|
float float_{0.0f};
|
||||||
std::string string_{};
|
std::string string_{};
|
||||||
int32_t int_{0};
|
int32_t int_{0};
|
||||||
FixedVector<bool> bool_array{};
|
std::vector<bool> bool_array{};
|
||||||
FixedVector<int32_t> int_array{};
|
std::vector<int32_t> int_array{};
|
||||||
FixedVector<float> float_array{};
|
std::vector<float> float_array{};
|
||||||
FixedVector<std::string> string_array{};
|
std::vector<std::string> string_array{};
|
||||||
void decode(const uint8_t *buffer, size_t length) override;
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void dump_to(std::string &out) const override;
|
void dump_to(std::string &out) const override;
|
||||||
#endif
|
#endif
|
||||||
@@ -1301,8 +1267,7 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
|
|||||||
const char *message_name() const override { return "execute_service_request"; }
|
const char *message_name() const override { return "execute_service_request"; }
|
||||||
#endif
|
#endif
|
||||||
uint32_t key{0};
|
uint32_t key{0};
|
||||||
FixedVector<ExecuteServiceArgument> args{};
|
std::vector<ExecuteServiceArgument> args{};
|
||||||
void decode(const uint8_t *buffer, size_t length) override;
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void dump_to(std::string &out) const override;
|
void dump_to(std::string &out) const override;
|
||||||
#endif
|
#endif
|
||||||
@@ -1371,28 +1336,27 @@ class CameraImageRequest final : public ProtoDecodableMessage {
|
|||||||
class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 46;
|
static constexpr uint8_t MESSAGE_TYPE = 46;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 150;
|
static constexpr uint8_t ESTIMATED_SIZE = 145;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "list_entities_climate_response"; }
|
const char *message_name() const override { return "list_entities_climate_response"; }
|
||||||
#endif
|
#endif
|
||||||
bool supports_current_temperature{false};
|
bool supports_current_temperature{false};
|
||||||
bool supports_two_point_target_temperature{false};
|
bool supports_two_point_target_temperature{false};
|
||||||
const climate::ClimateModeMask *supported_modes{};
|
const std::set<climate::ClimateMode> *supported_modes{};
|
||||||
float visual_min_temperature{0.0f};
|
float visual_min_temperature{0.0f};
|
||||||
float visual_max_temperature{0.0f};
|
float visual_max_temperature{0.0f};
|
||||||
float visual_target_temperature_step{0.0f};
|
float visual_target_temperature_step{0.0f};
|
||||||
bool supports_action{false};
|
bool supports_action{false};
|
||||||
const climate::ClimateFanModeMask *supported_fan_modes{};
|
const std::set<climate::ClimateFanMode> *supported_fan_modes{};
|
||||||
const climate::ClimateSwingModeMask *supported_swing_modes{};
|
const std::set<climate::ClimateSwingMode> *supported_swing_modes{};
|
||||||
const std::vector<const char *> *supported_custom_fan_modes{};
|
const std::set<std::string> *supported_custom_fan_modes{};
|
||||||
const climate::ClimatePresetMask *supported_presets{};
|
const std::set<climate::ClimatePreset> *supported_presets{};
|
||||||
const std::vector<const char *> *supported_custom_presets{};
|
const std::set<std::string> *supported_custom_presets{};
|
||||||
float visual_current_temperature_step{0.0f};
|
float visual_current_temperature_step{0.0f};
|
||||||
bool supports_current_humidity{false};
|
bool supports_current_humidity{false};
|
||||||
bool supports_target_humidity{false};
|
bool supports_target_humidity{false};
|
||||||
float visual_min_humidity{0.0f};
|
float visual_min_humidity{0.0f};
|
||||||
float visual_max_humidity{0.0f};
|
float visual_max_humidity{0.0f};
|
||||||
uint32_t feature_flags{0};
|
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@@ -1534,7 +1498,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
|
|||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "list_entities_select_response"; }
|
const char *message_name() const override { return "list_entities_select_response"; }
|
||||||
#endif
|
#endif
|
||||||
const FixedVector<const char *> *options{};
|
const std::vector<std::string> *options{};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@@ -1926,7 +1890,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage {
|
|||||||
std::array<uint64_t, 2> uuid{};
|
std::array<uint64_t, 2> uuid{};
|
||||||
uint32_t handle{0};
|
uint32_t handle{0};
|
||||||
uint32_t properties{0};
|
uint32_t properties{0};
|
||||||
FixedVector<BluetoothGATTDescriptor> descriptors{};
|
std::vector<BluetoothGATTDescriptor> descriptors{};
|
||||||
uint32_t short_uuid{0};
|
uint32_t short_uuid{0};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
@@ -1940,7 +1904,7 @@ class BluetoothGATTService final : public ProtoMessage {
|
|||||||
public:
|
public:
|
||||||
std::array<uint64_t, 2> uuid{};
|
std::array<uint64_t, 2> uuid{};
|
||||||
uint32_t handle{0};
|
uint32_t handle{0};
|
||||||
FixedVector<BluetoothGATTCharacteristic> characteristics{};
|
std::vector<BluetoothGATTCharacteristic> characteristics{};
|
||||||
uint32_t short_uuid{0};
|
uint32_t short_uuid{0};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
@@ -2788,7 +2752,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage {
|
|||||||
#endif
|
#endif
|
||||||
StringRef device_class_ref_{};
|
StringRef device_class_ref_{};
|
||||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||||
const FixedVector<const char *> *event_types{};
|
std::vector<std::string> event_types{};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
|||||||
@@ -88,12 +88,6 @@ static void dump_field(std::string &out, const char *field_name, StringRef value
|
|||||||
out.append("\n");
|
out.append("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) {
|
|
||||||
append_field_prefix(out, field_name, indent);
|
|
||||||
out.append("'").append(value).append("'");
|
|
||||||
out.append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
|
template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
|
||||||
append_field_prefix(out, field_name, indent);
|
append_field_prefix(out, field_name, indent);
|
||||||
out.append(proto_enum_to_string<T>(value));
|
out.append(proto_enum_to_string<T>(value));
|
||||||
@@ -1128,28 +1122,6 @@ void HomeassistantActionRequest::dump_to(std::string &out) const {
|
|||||||
out.append("\n");
|
out.append("\n");
|
||||||
}
|
}
|
||||||
dump_field(out, "is_event", this->is_event);
|
dump_field(out, "is_event", this->is_event);
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
dump_field(out, "call_id", this->call_id);
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
dump_field(out, "wants_response", this->wants_response);
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
dump_field(out, "response_template", this->response_template);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
void HomeassistantActionResponse::dump_to(std::string &out) const {
|
|
||||||
MessageDumpHelper helper(out, "HomeassistantActionResponse");
|
|
||||||
dump_field(out, "call_id", this->call_id);
|
|
||||||
dump_field(out, "success", this->success);
|
|
||||||
dump_field(out, "error_message", this->error_message);
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
out.append(" response_data: ");
|
|
||||||
out.append(format_hex_pretty(this->response_data, this->response_data_len));
|
|
||||||
out.append("\n");
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
@@ -1298,7 +1270,6 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
|
|||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
dump_field(out, "device_id", this->device_id);
|
dump_field(out, "device_id", this->device_id);
|
||||||
#endif
|
#endif
|
||||||
dump_field(out, "feature_flags", this->feature_flags);
|
|
||||||
}
|
}
|
||||||
void ClimateStateResponse::dump_to(std::string &out) const {
|
void ClimateStateResponse::dump_to(std::string &out) const {
|
||||||
MessageDumpHelper helper(out, "ClimateStateResponse");
|
MessageDumpHelper helper(out, "ClimateStateResponse");
|
||||||
@@ -2053,7 +2024,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const {
|
|||||||
dump_field(out, "disabled_by_default", this->disabled_by_default);
|
dump_field(out, "disabled_by_default", this->disabled_by_default);
|
||||||
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
|
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
|
||||||
dump_field(out, "device_class", this->device_class_ref_);
|
dump_field(out, "device_class", this->device_class_ref_);
|
||||||
for (const auto &it : *this->event_types) {
|
for (const auto &it : this->event_types) {
|
||||||
dump_field(out, "event_types", it, 4);
|
dump_field(out, "event_types", it, 4);
|
||||||
}
|
}
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
|
|||||||
@@ -610,17 +610,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
this->on_z_wave_proxy_request(msg);
|
this->on_z_wave_proxy_request(msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
case HomeassistantActionResponse::MESSAGE_TYPE: {
|
|
||||||
HomeassistantActionResponse msg;
|
|
||||||
msg.decode(msg_data, msg_size);
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
|
||||||
ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str());
|
|
||||||
#endif
|
|
||||||
this->on_homeassistant_action_response(msg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -66,9 +66,6 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
|
|
||||||
#endif
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -5,21 +5,16 @@
|
|||||||
#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/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
#include "esphome/core/controller_registry.h"
|
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/util.h"
|
#include "esphome/core/util.h"
|
||||||
#include "esphome/core/version.h"
|
#include "esphome/core/version.h"
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
|
||||||
#include "homeassistant_service.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_LOGGER
|
#ifdef USE_LOGGER
|
||||||
#include "esphome/components/logger/logger.h"
|
#include "esphome/components/logger/logger.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
@@ -35,7 +30,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;
|
||||||
@@ -225,7 +220,7 @@ 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()) {
|
||||||
@@ -270,7 +265,7 @@ bool APIServer::check_password(const uint8_t *password_data, size_t password_len
|
|||||||
|
|
||||||
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()) \
|
||||||
@@ -279,6 +274,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
|
||||||
@@ -296,15 +300,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
|
||||||
@@ -312,7 +316,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
|
||||||
@@ -328,11 +332,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
|
||||||
@@ -348,13 +352,12 @@ API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
|
|||||||
#endif
|
#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
|
||||||
|
|
||||||
@@ -397,38 +400,7 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
|
|||||||
client->send_homeassistant_action(call);
|
client->send_homeassistant_action(call);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
#endif
|
||||||
void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) {
|
|
||||||
this->action_response_callbacks_.push_back({call_id, std::move(callback)});
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if (it->call_id == call_id) {
|
|
||||||
auto callback = std::move(it->callback);
|
|
||||||
this->action_response_callbacks_.erase(it);
|
|
||||||
ActionResponse response(success, error_message);
|
|
||||||
callback(response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
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) {
|
|
||||||
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
|
|
||||||
if (it->call_id == call_id) {
|
|
||||||
auto callback = std::move(it->callback);
|
|
||||||
this->action_response_callbacks_.erase(it);
|
|
||||||
ActionResponse response(success, error_message, response_data, response_data_len);
|
|
||||||
callback(response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||||
@@ -461,31 +433,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
|
||||||
@@ -500,21 +447,27 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
#include "user_services.h"
|
#include "user_services.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
@@ -53,7 +52,6 @@ class APIServer : public Component, public Controller {
|
|||||||
|
|
||||||
#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) { noise_ctx_->set_psk(psk); }
|
||||||
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
|
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
|
||||||
#endif // USE_API_NOISE
|
#endif // USE_API_NOISE
|
||||||
@@ -72,19 +70,19 @@ class APIServer : public Component, public Controller {
|
|||||||
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;
|
||||||
@@ -96,10 +94,10 @@ class APIServer : public Component, public Controller {
|
|||||||
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;
|
||||||
@@ -113,25 +111,9 @@ class APIServer : public Component, public Controller {
|
|||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
void send_homeassistant_action(const HomeassistantActionRequest &call);
|
void send_homeassistant_action(const HomeassistantActionRequest &call);
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
// Action response handling
|
|
||||||
using ActionResponseCallback = std::function<void(const class ActionResponse &)>;
|
|
||||||
void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback);
|
|
||||||
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message);
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
|
|
||||||
const uint8_t *response_data, size_t response_data_len);
|
|
||||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
|
||||||
#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); }
|
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_API_SERVICES
|
||||||
|
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_HOMEASSISTANT_TIME
|
#ifdef USE_HOMEASSISTANT_TIME
|
||||||
void request_time();
|
void request_time();
|
||||||
@@ -141,7 +123,7 @@ class APIServer : public Component, public Controller {
|
|||||||
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;
|
||||||
@@ -181,10 +163,6 @@ class APIServer : public Component, public Controller {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
void schedule_reboot_timeout_();
|
void schedule_reboot_timeout_();
|
||||||
#ifdef USE_API_NOISE
|
|
||||||
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
|
|
||||||
// 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
|
||||||
@@ -209,13 +187,6 @@ class APIServer : public Component, public Controller {
|
|||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
std::vector<UserServiceDescriptor *> user_services_;
|
std::vector<UserServiceDescriptor *> user_services_;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
struct PendingActionResponse {
|
|
||||||
uint32_t call_id;
|
|
||||||
ActionResponseCallback callback;
|
|
||||||
};
|
|
||||||
std::vector<PendingActionResponse> action_response_callbacks_;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Group smaller types together
|
// Group smaller types together
|
||||||
uint16_t port_{6053};
|
uint16_t port_{6053};
|
||||||
@@ -237,7 +208,7 @@ extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-cons
|
|||||||
|
|
||||||
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
|
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
|
||||||
public:
|
public:
|
||||||
bool check(const Ts &...x) override { return global_api_server->is_connected(); }
|
bool check(Ts... x) override { return global_api_server->is_connected(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
#ifdef USE_API_SERVICES
|
#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:
|
||||||
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
|
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
|
||||||
@@ -53,14 +53,8 @@ class CustomAPIDevice {
|
|||||||
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>
|
||||||
@@ -92,14 +86,8 @@ class CustomAPIDevice {
|
|||||||
*/
|
*/
|
||||||
#ifdef USE_API_SERVICES
|
#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) {
|
||||||
@@ -213,9 +201,9 @@ 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.set_service(StringRef(service_name));
|
resp.set_service(StringRef(service_name));
|
||||||
resp.data.init(data.size());
|
|
||||||
for (auto &it : data) {
|
for (auto &it : data) {
|
||||||
auto &kv = resp.data.emplace_back();
|
resp.data.emplace_back();
|
||||||
|
auto &kv = resp.data.back();
|
||||||
kv.set_key(StringRef(it.first));
|
kv.set_key(StringRef(it.first));
|
||||||
kv.value = it.second;
|
kv.value = it.second;
|
||||||
}
|
}
|
||||||
@@ -256,9 +244,9 @@ class CustomAPIDevice {
|
|||||||
HomeassistantActionRequest resp;
|
HomeassistantActionRequest resp;
|
||||||
resp.set_service(StringRef(service_name));
|
resp.set_service(StringRef(service_name));
|
||||||
resp.is_event = true;
|
resp.is_event = true;
|
||||||
resp.data.init(data.size());
|
|
||||||
for (auto &it : data) {
|
for (auto &it : data) {
|
||||||
auto &kv = resp.data.emplace_back();
|
resp.data.emplace_back();
|
||||||
|
auto &kv = resp.data.back();
|
||||||
kv.set_key(StringRef(it.first));
|
kv.set_key(StringRef(it.first));
|
||||||
kv.value = it.second;
|
kv.value = it.second;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,8 @@
|
|||||||
#include "api_server.h"
|
#include "api_server.h"
|
||||||
#ifdef USE_API
|
#ifdef USE_API
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
#include <functional>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "api_pb2.h"
|
#include "api_pb2.h"
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
#include "esphome/components/json/json_util.h"
|
|
||||||
#endif
|
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
@@ -41,191 +36,66 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
|||||||
|
|
||||||
template<typename... Ts> class TemplatableKeyValuePair {
|
template<typename... Ts> class TemplatableKeyValuePair {
|
||||||
public:
|
public:
|
||||||
// Default constructor needed for FixedVector::emplace_back()
|
|
||||||
TemplatableKeyValuePair() = default;
|
|
||||||
|
|
||||||
// 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 pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
|
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
|
||||||
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
|
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
|
||||||
|
|
||||||
std::string key;
|
std::string key;
|
||||||
TemplatableStringValue<Ts...> value;
|
TemplatableStringValue<Ts...> value;
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
// Represents the response data from a Home Assistant action
|
|
||||||
class ActionResponse {
|
|
||||||
public:
|
|
||||||
ActionResponse(bool success, std::string error_message = "")
|
|
||||||
: success_(success), error_message_(std::move(error_message)) {}
|
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
|
|
||||||
: success_(success), error_message_(std::move(error_message)) {
|
|
||||||
if (data == nullptr || data_len == 0)
|
|
||||||
return;
|
|
||||||
this->json_document_ = json::parse_json(data, data_len);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
bool is_success() const { return this->success_; }
|
|
||||||
const std::string &get_error_message() const { return this->error_message_; }
|
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
// Get data as parsed JSON object (const version returns read-only view)
|
|
||||||
JsonObjectConst get_json() const { return this->json_document_.as<JsonObjectConst>(); }
|
|
||||||
#endif
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool success_;
|
|
||||||
std::string error_message_;
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
JsonDocument json_document_;
|
|
||||||
#endif
|
|
||||||
};
|
|
||||||
|
|
||||||
// Callback type for action responses
|
|
||||||
template<typename... Ts> using ActionResponseCallback = std::function<void(const ActionResponse &, Ts...)>;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
|
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
|
||||||
public:
|
public:
|
||||||
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) {
|
explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {}
|
||||||
this->flags_.is_event = is_event;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T> void set_service(T service) { this->service_ = service; }
|
template<typename T> void set_service(T service) { this->service_ = service; }
|
||||||
|
|
||||||
// Initialize FixedVector members - called from Python codegen with compile-time known sizes.
|
|
||||||
// Must be called before any add_* methods; capacity must match the number of subsequent add_* calls.
|
|
||||||
void init_data(size_t count) { this->data_.init(count); }
|
|
||||||
void init_data_template(size_t count) { this->data_template_.init(count); }
|
|
||||||
void init_variables(size_t count) { this->variables_.init(count); }
|
|
||||||
|
|
||||||
// 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.
|
||||||
template<typename K, typename V> void add_data(K &&key, V &&value) {
|
// Using pass-by-value allows the compiler to optimize for both lvalues and rvalues.
|
||||||
this->add_kv_(this->data_, std::forward<K>(key), std::forward<V>(value));
|
template<typename T> void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); }
|
||||||
|
template<typename T> void add_data_template(std::string key, T value) {
|
||||||
|
this->data_template_.emplace_back(std::move(key), value);
|
||||||
}
|
}
|
||||||
template<typename K, typename V> void add_data_template(K &&key, V &&value) {
|
template<typename T> void add_variable(std::string key, T value) {
|
||||||
this->add_kv_(this->data_template_, std::forward<K>(key), std::forward<V>(value));
|
this->variables_.emplace_back(std::move(key), value);
|
||||||
}
|
|
||||||
template<typename K, typename V> void add_variable(K &&key, V &&value) {
|
|
||||||
this->add_kv_(this->variables_, std::forward<K>(key), std::forward<V>(value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
void play(Ts... x) override {
|
||||||
template<typename T> void set_response_template(T response_template) {
|
|
||||||
this->response_template_ = response_template;
|
|
||||||
this->flags_.has_response_template = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void set_wants_status() { this->flags_.wants_status = true; }
|
|
||||||
void set_wants_response() { this->flags_.wants_response = true; }
|
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() const {
|
|
||||||
return this->success_trigger_with_response_;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; }
|
|
||||||
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
|
|
||||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
|
|
||||||
void play(const Ts &...x) override {
|
|
||||||
HomeassistantActionRequest resp;
|
HomeassistantActionRequest resp;
|
||||||
std::string service_value = this->service_.value(x...);
|
std::string service_value = this->service_.value(x...);
|
||||||
resp.set_service(StringRef(service_value));
|
resp.set_service(StringRef(service_value));
|
||||||
resp.is_event = this->flags_.is_event;
|
resp.is_event = this->is_event_;
|
||||||
this->populate_service_map(resp.data, this->data_, x...);
|
for (auto &it : this->data_) {
|
||||||
this->populate_service_map(resp.data_template, this->data_template_, x...);
|
resp.data.emplace_back();
|
||||||
this->populate_service_map(resp.variables, this->variables_, x...);
|
auto &kv = resp.data.back();
|
||||||
|
kv.set_key(StringRef(it.key));
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
kv.value = it.value.value(x...);
|
||||||
if (this->flags_.wants_status) {
|
}
|
||||||
// Generate a unique call ID for this service call
|
for (auto &it : this->data_template_) {
|
||||||
static uint32_t call_id_counter = 1;
|
resp.data_template.emplace_back();
|
||||||
uint32_t call_id = call_id_counter++;
|
auto &kv = resp.data_template.back();
|
||||||
resp.call_id = call_id;
|
kv.set_key(StringRef(it.key));
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
kv.value = it.value.value(x...);
|
||||||
if (this->flags_.wants_response) {
|
}
|
||||||
resp.wants_response = true;
|
for (auto &it : this->variables_) {
|
||||||
// Set response template if provided
|
resp.variables.emplace_back();
|
||||||
if (this->flags_.has_response_template) {
|
auto &kv = resp.variables.back();
|
||||||
std::string response_template_value = this->response_template_.value(x...);
|
kv.set_key(StringRef(it.key));
|
||||||
resp.response_template = response_template_value;
|
kv.value = it.value.value(x...);
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
auto captured_args = std::make_tuple(x...);
|
|
||||||
this->parent_->register_action_response_callback(call_id, [this, captured_args](const ActionResponse &response) {
|
|
||||||
std::apply(
|
|
||||||
[this, &response](auto &&...args) {
|
|
||||||
if (response.is_success()) {
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
if (this->flags_.wants_response) {
|
|
||||||
this->success_trigger_with_response_->trigger(response.get_json(), args...);
|
|
||||||
} else
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
this->success_trigger_->trigger(args...);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this->error_trigger_->trigger(response.get_error_message(), args...);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
captured_args);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
this->parent_->send_homeassistant_action(resp);
|
this->parent_->send_homeassistant_action(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies
|
|
||||||
template<typename K, typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, K &&key, V &&value) {
|
|
||||||
auto &kv = vec.emplace_back();
|
|
||||||
kv.key = std::forward<K>(key);
|
|
||||||
kv.value = std::forward<V>(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename VectorType, typename SourceType>
|
|
||||||
static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) {
|
|
||||||
dest.init(source.size());
|
|
||||||
for (auto &it : source) {
|
|
||||||
auto &kv = dest.emplace_back();
|
|
||||||
kv.set_key(StringRef(it.key));
|
|
||||||
kv.value = it.value.value(x...);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
APIServer *parent_;
|
APIServer *parent_;
|
||||||
|
bool is_event_;
|
||||||
TemplatableStringValue<Ts...> service_{};
|
TemplatableStringValue<Ts...> service_{};
|
||||||
FixedVector<TemplatableKeyValuePair<Ts...>> data_;
|
std::vector<TemplatableKeyValuePair<Ts...>> data_;
|
||||||
FixedVector<TemplatableKeyValuePair<Ts...>> data_template_;
|
std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
|
||||||
FixedVector<TemplatableKeyValuePair<Ts...>> variables_;
|
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
TemplatableStringValue<Ts...> response_template_{""};
|
|
||||||
Trigger<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, Ts...>();
|
|
||||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
|
||||||
Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>();
|
|
||||||
Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>();
|
|
||||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
|
||||||
|
|
||||||
struct Flags {
|
|
||||||
uint8_t is_event : 1;
|
|
||||||
uint8_t wants_status : 1;
|
|
||||||
uint8_t wants_response : 1;
|
|
||||||
uint8_t has_response_template : 1;
|
|
||||||
uint8_t reserved : 5;
|
|
||||||
} flags_{0};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -7,69 +7,6 @@ namespace esphome::api {
|
|||||||
|
|
||||||
static const char *const TAG = "api.proto";
|
static const char *const TAG = "api.proto";
|
||||||
|
|
||||||
uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
|
|
||||||
uint32_t count = 0;
|
|
||||||
const uint8_t *ptr = buffer;
|
|
||||||
const uint8_t *end = buffer + length;
|
|
||||||
|
|
||||||
while (ptr < end) {
|
|
||||||
uint32_t consumed;
|
|
||||||
|
|
||||||
// Parse field header (tag)
|
|
||||||
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
|
||||||
if (!res.has_value()) {
|
|
||||||
break; // Invalid data, stop counting
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t tag = res->as_uint32();
|
|
||||||
uint32_t field_type = tag & WIRE_TYPE_MASK;
|
|
||||||
uint32_t field_id = tag >> 3;
|
|
||||||
ptr += consumed;
|
|
||||||
|
|
||||||
// Count if this is the target field
|
|
||||||
if (field_id == target_field_id) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip field data based on wire type
|
|
||||||
switch (field_type) {
|
|
||||||
case WIRE_TYPE_VARINT: { // VarInt - parse and skip
|
|
||||||
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
|
||||||
if (!res.has_value()) {
|
|
||||||
return count; // Invalid data, return what we have
|
|
||||||
}
|
|
||||||
ptr += consumed;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data
|
|
||||||
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
|
||||||
if (!res.has_value()) {
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
uint32_t field_length = res->as_uint32();
|
|
||||||
ptr += consumed;
|
|
||||||
if (ptr + field_length > end) {
|
|
||||||
return count; // Out of bounds
|
|
||||||
}
|
|
||||||
ptr += field_length;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
|
|
||||||
if (ptr + 4 > end) {
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
ptr += 4;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// Unknown wire type, can't continue
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||||
const uint8_t *ptr = buffer;
|
const uint8_t *ptr = buffer;
|
||||||
const uint8_t *end = buffer + length;
|
const uint8_t *end = buffer + length;
|
||||||
@@ -85,12 +22,12 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t tag = res->as_uint32();
|
uint32_t tag = res->as_uint32();
|
||||||
uint32_t field_type = tag & WIRE_TYPE_MASK;
|
uint32_t field_type = tag & 0b111;
|
||||||
uint32_t field_id = tag >> 3;
|
uint32_t field_id = tag >> 3;
|
||||||
ptr += consumed;
|
ptr += consumed;
|
||||||
|
|
||||||
switch (field_type) {
|
switch (field_type) {
|
||||||
case WIRE_TYPE_VARINT: { // VarInt
|
case 0: { // VarInt
|
||||||
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||||
if (!res.has_value()) {
|
if (!res.has_value()) {
|
||||||
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
||||||
@@ -102,7 +39,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
|||||||
ptr += consumed;
|
ptr += consumed;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
|
case 2: { // Length-delimited
|
||||||
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||||
if (!res.has_value()) {
|
if (!res.has_value()) {
|
||||||
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||||
@@ -120,7 +57,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
|||||||
ptr += field_length;
|
ptr += field_length;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WIRE_TYPE_FIXED32: { // 32-bit
|
case 5: { // 32-bit
|
||||||
if (ptr + 4 > end) {
|
if (ptr + 4 > end) {
|
||||||
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -15,13 +15,6 @@
|
|||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
// Protocol Buffer wire type constants
|
|
||||||
// See https://protobuf.dev/programming-guides/encoding/#structure
|
|
||||||
constexpr uint8_t WIRE_TYPE_VARINT = 0; // int32, int64, uint32, uint64, sint32, sint64, bool, enum
|
|
||||||
constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED = 2; // string, bytes, embedded messages, packed repeated fields
|
|
||||||
constexpr uint8_t WIRE_TYPE_FIXED32 = 5; // fixed32, sfixed32, float
|
|
||||||
constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type from tag
|
|
||||||
|
|
||||||
// Helper functions for ZigZag encoding/decoding
|
// Helper functions for ZigZag encoding/decoding
|
||||||
inline constexpr uint32_t encode_zigzag32(int32_t value) {
|
inline constexpr uint32_t encode_zigzag32(int32_t value) {
|
||||||
return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
||||||
@@ -248,7 +241,7 @@ class ProtoWriteBuffer {
|
|||||||
* Following https://protobuf.dev/programming-guides/encoding/#structure
|
* Following https://protobuf.dev/programming-guides/encoding/#structure
|
||||||
*/
|
*/
|
||||||
void encode_field_raw(uint32_t field_id, uint32_t type) {
|
void encode_field_raw(uint32_t field_id, uint32_t type) {
|
||||||
uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK);
|
uint32_t val = (field_id << 3) | (type & 0b111);
|
||||||
this->encode_varint_raw(val);
|
this->encode_varint_raw(val);
|
||||||
}
|
}
|
||||||
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
|
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
|
||||||
@@ -361,18 +354,7 @@ class ProtoMessage {
|
|||||||
// Base class for messages that support decoding
|
// Base class for messages that support decoding
|
||||||
class ProtoDecodableMessage : public ProtoMessage {
|
class ProtoDecodableMessage : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
virtual void decode(const uint8_t *buffer, size_t length);
|
void decode(const uint8_t *buffer, size_t length);
|
||||||
|
|
||||||
/**
|
|
||||||
* Count occurrences of a repeated field in a protobuf buffer.
|
|
||||||
* This is a lightweight scan that only parses tags and skips field data.
|
|
||||||
*
|
|
||||||
* @param buffer Pointer to the protobuf buffer
|
|
||||||
* @param length Length of the buffer in bytes
|
|
||||||
* @param target_field_id The field ID to count
|
|
||||||
* @return Number of times the field appears in the buffer
|
|
||||||
*/
|
|
||||||
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
|
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
|
||||||
@@ -500,7 +482,7 @@ class ProtoSize {
|
|||||||
* @return The number of bytes needed to encode the field ID and wire type
|
* @return The number of bytes needed to encode the field ID and wire type
|
||||||
*/
|
*/
|
||||||
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
|
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
|
||||||
uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK);
|
uint32_t tag = (field_id << 3) | (type & 0b111);
|
||||||
return varint(tag);
|
return varint(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,29 +749,13 @@ class ProtoSize {
|
|||||||
template<typename MessageType>
|
template<typename MessageType>
|
||||||
inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) {
|
inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) {
|
||||||
// Skip if the vector is empty
|
// Skip if the vector is empty
|
||||||
if (!messages.empty()) {
|
if (messages.empty()) {
|
||||||
// Use the force version for all messages in the repeated field
|
return;
|
||||||
for (const auto &message : messages) {
|
|
||||||
add_message_object_force(field_id_size, message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Use the force version for all messages in the repeated field
|
||||||
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector
|
for (const auto &message : messages) {
|
||||||
* version)
|
add_message_object_force(field_id_size, message);
|
||||||
*
|
|
||||||
* @tparam MessageType The type of the nested messages in the FixedVector
|
|
||||||
* @param messages FixedVector of message objects
|
|
||||||
*/
|
|
||||||
template<typename MessageType>
|
|
||||||
inline void add_repeated_message(uint32_t field_id_size, const FixedVector<MessageType> &messages) {
|
|
||||||
// Skip if the fixed vector is empty
|
|
||||||
if (!messages.empty()) {
|
|
||||||
// Use the force version for all messages in the repeated field
|
|
||||||
for (const auto &message : messages) {
|
|
||||||
add_message_object_force(field_id_size, message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,49 +11,16 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
|
|||||||
}
|
}
|
||||||
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
|
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
|
||||||
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
||||||
|
|
||||||
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
|
|
||||||
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
|
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
|
||||||
std::vector<bool> result;
|
|
||||||
result.reserve(arg.bool_array.size());
|
|
||||||
result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
|
|
||||||
std::vector<int32_t> result;
|
|
||||||
result.reserve(arg.int_array.size());
|
|
||||||
result.insert(result.end(), arg.int_array.begin(), arg.int_array.end());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
|
|
||||||
std::vector<float> result;
|
|
||||||
result.reserve(arg.float_array.size());
|
|
||||||
result.insert(result.end(), arg.float_array.begin(), arg.float_array.end());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
|
|
||||||
std::vector<std::string> result;
|
|
||||||
result.reserve(arg.string_array.size());
|
|
||||||
result.insert(result.end(), arg.string_array.begin(), arg.string_array.end());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// New FixedVector const reference versions for YAML-generated services - zero-copy
|
|
||||||
template<>
|
|
||||||
const FixedVector<bool> &get_execute_arg_value<const FixedVector<bool> &>(const ExecuteServiceArgument &arg) {
|
|
||||||
return arg.bool_array;
|
return arg.bool_array;
|
||||||
}
|
}
|
||||||
template<>
|
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
|
||||||
const FixedVector<int32_t> &get_execute_arg_value<const FixedVector<int32_t> &>(const ExecuteServiceArgument &arg) {
|
|
||||||
return arg.int_array;
|
return arg.int_array;
|
||||||
}
|
}
|
||||||
template<>
|
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
|
||||||
const FixedVector<float> &get_execute_arg_value<const FixedVector<float> &>(const ExecuteServiceArgument &arg) {
|
|
||||||
return arg.float_array;
|
return arg.float_array;
|
||||||
}
|
}
|
||||||
template<>
|
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
|
||||||
const FixedVector<std::string> &get_execute_arg_value<const FixedVector<std::string> &>(
|
|
||||||
const ExecuteServiceArgument &arg) {
|
|
||||||
return arg.string_array;
|
return arg.string_array;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,8 +28,6 @@ template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SER
|
|||||||
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
|
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
|
||||||
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
|
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
|
||||||
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
||||||
|
|
||||||
// Legacy std::vector versions for external components using custom_api_device.h
|
|
||||||
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
|
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
|
||||||
template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() {
|
template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() {
|
||||||
return enums::SERVICE_ARG_TYPE_INT_ARRAY;
|
return enums::SERVICE_ARG_TYPE_INT_ARRAY;
|
||||||
@@ -74,18 +39,4 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>()
|
|||||||
return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
|
return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// New FixedVector const reference versions for YAML-generated services
|
|
||||||
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<bool> &>() {
|
|
||||||
return enums::SERVICE_ARG_TYPE_BOOL_ARRAY;
|
|
||||||
}
|
|
||||||
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<int32_t> &>() {
|
|
||||||
return enums::SERVICE_ARG_TYPE_INT_ARRAY;
|
|
||||||
}
|
|
||||||
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<float> &>() {
|
|
||||||
return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY;
|
|
||||||
}
|
|
||||||
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<std::string> &>() {
|
|
||||||
return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
|||||||
@@ -23,57 +23,11 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
|
|||||||
|
|
||||||
template<typename T> enums::ServiceArgType to_service_arg_type();
|
template<typename T> enums::ServiceArgType to_service_arg_type();
|
||||||
|
|
||||||
// Base class for YAML-defined services (most common case)
|
|
||||||
// Stores only pointers to string literals in flash - no heap allocation
|
|
||||||
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||||
public:
|
public:
|
||||||
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
UserServiceBase(std::string name, const std::array<std::string, sizeof...(Ts)> &arg_names)
|
||||||
: name_(name), arg_names_(arg_names) {
|
|
||||||
this->key_ = fnv1_hash(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
|
||||||
ListEntitiesServicesResponse msg;
|
|
||||||
msg.set_name(StringRef(this->name_));
|
|
||||||
msg.key = this->key_;
|
|
||||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
|
||||||
msg.args.init(sizeof...(Ts));
|
|
||||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
|
||||||
auto &arg = msg.args.emplace_back();
|
|
||||||
arg.type = arg_types[i];
|
|
||||||
arg.set_name(StringRef(this->arg_names_[i]));
|
|
||||||
}
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool execute_service(const ExecuteServiceRequest &req) override {
|
|
||||||
if (req.key != this->key_)
|
|
||||||
return false;
|
|
||||||
if (req.args.size() != sizeof...(Ts))
|
|
||||||
return false;
|
|
||||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual void execute(Ts... x) = 0;
|
|
||||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
|
||||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pointers to string literals in flash - no heap allocation
|
|
||||||
const char *name_;
|
|
||||||
std::array<const char *, sizeof...(Ts)> arg_names_;
|
|
||||||
uint32_t key_{0};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Separate class for custom_api_device services (rare case)
|
|
||||||
// Stores copies of runtime-generated names
|
|
||||||
template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor {
|
|
||||||
public:
|
|
||||||
UserServiceDynamic(std::string name, const std::array<std::string, sizeof...(Ts)> &arg_names)
|
|
||||||
: name_(std::move(name)), arg_names_(arg_names) {
|
: name_(std::move(name)), arg_names_(arg_names) {
|
||||||
this->key_ = fnv1_hash(this->name_.c_str());
|
this->key_ = fnv1_hash(this->name_);
|
||||||
}
|
}
|
||||||
|
|
||||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||||
@@ -81,9 +35,9 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
|||||||
msg.set_name(StringRef(this->name_));
|
msg.set_name(StringRef(this->name_));
|
||||||
msg.key = this->key_;
|
msg.key = this->key_;
|
||||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||||
msg.args.init(sizeof...(Ts));
|
for (int i = 0; i < sizeof...(Ts); i++) {
|
||||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
msg.args.emplace_back();
|
||||||
auto &arg = msg.args.emplace_back();
|
auto &arg = msg.args.back();
|
||||||
arg.type = arg_types[i];
|
arg.type = arg_types[i];
|
||||||
arg.set_name(StringRef(this->arg_names_[i]));
|
arg.set_name(StringRef(this->arg_names_[i]));
|
||||||
}
|
}
|
||||||
@@ -93,7 +47,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
|||||||
bool execute_service(const ExecuteServiceRequest &req) override {
|
bool execute_service(const ExecuteServiceRequest &req) override {
|
||||||
if (req.key != this->key_)
|
if (req.key != this->key_)
|
||||||
return false;
|
return false;
|
||||||
if (req.args.size() != sizeof...(Ts))
|
if (req.args.size() != this->arg_names_.size())
|
||||||
return false;
|
return false;
|
||||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||||
return true;
|
return true;
|
||||||
@@ -101,20 +55,18 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void execute(Ts... x) = 0;
|
virtual void execute(Ts... x) = 0;
|
||||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
template<int... S> void execute_(const std::vector<ExecuteServiceArgument> &args, seq<S...> type) {
|
||||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heap-allocated strings for runtime-generated names
|
|
||||||
std::string name_;
|
std::string name_;
|
||||||
std::array<std::string, sizeof...(Ts)> arg_names_;
|
|
||||||
uint32_t key_{0};
|
uint32_t key_{0};
|
||||||
|
std::array<std::string, sizeof...(Ts)> arg_names_;
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
|
template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
|
||||||
public:
|
public:
|
||||||
// Constructor for static names (YAML-defined services - used by code generator)
|
UserServiceTrigger(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names)
|
||||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
|
||||||
: UserServiceBase<Ts...>(name, arg_names) {}
|
: UserServiceBase<Ts...>(name, arg_names) {}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace at581x {
|
|||||||
|
|
||||||
template<typename... Ts> class AT581XResetAction : public Action<Ts...>, public Parented<AT581XComponent> {
|
template<typename... Ts> class AT581XResetAction : public Action<Ts...>, public Parented<AT581XComponent> {
|
||||||
public:
|
public:
|
||||||
void play(const Ts &...x) { this->parent_->reset_hardware_frontend(); }
|
void play(Ts... x) { this->parent_->reset_hardware_frontend(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, public Parented<AT581XComponent> {
|
template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, public Parented<AT581XComponent> {
|
||||||
@@ -25,7 +25,7 @@ template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, publ
|
|||||||
TEMPLATABLE_VALUE(int, trigger_keep)
|
TEMPLATABLE_VALUE(int, trigger_keep)
|
||||||
TEMPLATABLE_VALUE(int, stage_gain)
|
TEMPLATABLE_VALUE(int, stage_gain)
|
||||||
|
|
||||||
void play(const Ts &...x) {
|
void play(Ts... x) {
|
||||||
if (this->frequency_.has_value()) {
|
if (this->frequency_.has_value()) {
|
||||||
int v = this->frequency_.value(x...);
|
int v = this->frequency_.value(x...);
|
||||||
this->parent_->set_frequency(v);
|
this->parent_->set_frequency(v);
|
||||||
|
|||||||
@@ -165,4 +165,4 @@ def final_validate_audio_schema(
|
|||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_library("esphome/esp-audio-libs", "2.0.1")
|
cg.add_library("esphome/esp-audio-libs", "1.1.4")
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const char *audio_file_type_to_string(AudioFileType file_type) {
|
|||||||
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
|
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
|
||||||
size_t samples_to_scale) {
|
size_t samples_to_scale) {
|
||||||
// Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same.
|
// Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same.
|
||||||
for (size_t i = 0; i < samples_to_scale; i++) {
|
for (int i = 0; i < samples_to_scale; i++) {
|
||||||
int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor;
|
int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor;
|
||||||
output_buffer[i] = (int16_t) (acc >> 15);
|
output_buffer[i] = (int16_t) (acc >> 15);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,18 +229,18 @@ FileDecoderState AudioDecoder::decode_flac_() {
|
|||||||
auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(),
|
auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(),
|
||||||
this->input_transfer_buffer_->available());
|
this->input_transfer_buffer_->available());
|
||||||
|
|
||||||
if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
|
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
|
||||||
// Serrious error reading FLAC header, there is no recovery
|
return FileDecoderState::POTENTIALLY_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != esp_audio_libs::flac::FLAC_DECODER_SUCCESS) {
|
||||||
|
// Couldn't read FLAC header
|
||||||
return FileDecoderState::FAILED;
|
return FileDecoderState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
|
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
|
||||||
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
|
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
|
||||||
|
|
||||||
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
|
|
||||||
return FileDecoderState::MORE_TO_PROCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reallocate the output transfer buffer to the smallest necessary size
|
// Reallocate the output transfer buffer to the smallest necessary size
|
||||||
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
|
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
|
||||||
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
|
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
|
||||||
@@ -256,9 +256,9 @@ FileDecoderState AudioDecoder::decode_flac_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t output_samples = 0;
|
uint32_t output_samples = 0;
|
||||||
auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(),
|
auto result = this->flac_decoder_->decode_frame(
|
||||||
this->input_transfer_buffer_->available(),
|
this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(),
|
||||||
this->output_transfer_buffer_->get_buffer_end(), &output_samples);
|
reinterpret_cast<int16_t *>(this->output_transfer_buffer_->get_buffer_end()), &output_samples);
|
||||||
|
|
||||||
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
|
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
|
||||||
// Not an issue, just needs more data that we'll get next time.
|
// Not an issue, just needs more data that we'll get next time.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ template<typename... Ts> class SetMicGainAction : public Action<Ts...> {
|
|||||||
|
|
||||||
TEMPLATABLE_VALUE(float, mic_gain)
|
TEMPLATABLE_VALUE(float, mic_gain)
|
||||||
|
|
||||||
void play(const Ts &...x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); }
|
void play(Ts... x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
AudioAdc *audio_adc_;
|
AudioAdc *audio_adc_;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ template<typename... Ts> class MuteOffAction : public Action<Ts...> {
|
|||||||
public:
|
public:
|
||||||
explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
|
explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
|
||||||
|
|
||||||
void play(const Ts &...x) override { this->audio_dac_->set_mute_off(); }
|
void play(Ts... x) override { this->audio_dac_->set_mute_off(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
AudioDac *audio_dac_;
|
AudioDac *audio_dac_;
|
||||||
@@ -21,7 +21,7 @@ template<typename... Ts> class MuteOnAction : public Action<Ts...> {
|
|||||||
public:
|
public:
|
||||||
explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
|
explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
|
||||||
|
|
||||||
void play(const Ts &...x) override { this->audio_dac_->set_mute_on(); }
|
void play(Ts... x) override { this->audio_dac_->set_mute_on(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
AudioDac *audio_dac_;
|
AudioDac *audio_dac_;
|
||||||
@@ -33,7 +33,7 @@ template<typename... Ts> class SetVolumeAction : public Action<Ts...> {
|
|||||||
|
|
||||||
TEMPLATABLE_VALUE(float, volume)
|
TEMPLATABLE_VALUE(float, volume)
|
||||||
|
|
||||||
void play(const Ts &...x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); }
|
void play(Ts... x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
AudioDac *audio_dac_;
|
AudioDac *audio_dac_;
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ namespace bang_bang {
|
|||||||
|
|
||||||
static const char *const TAG = "bang_bang.climate";
|
static const char *const TAG = "bang_bang.climate";
|
||||||
|
|
||||||
BangBangClimate::BangBangClimate()
|
|
||||||
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
|
|
||||||
|
|
||||||
void BangBangClimate::setup() {
|
void BangBangClimate::setup() {
|
||||||
this->sensor_->add_on_state_callback([this](float state) {
|
this->sensor_->add_on_state_callback([this](float state) {
|
||||||
this->current_temperature = state;
|
this->current_temperature = state;
|
||||||
@@ -34,63 +31,53 @@ void BangBangClimate::setup() {
|
|||||||
restore->to_call(this).perform();
|
restore->to_call(this).perform();
|
||||||
} else {
|
} else {
|
||||||
// restore from defaults, change_away handles those for us
|
// restore from defaults, change_away handles those for us
|
||||||
if (this->supports_cool_ && this->supports_heat_) {
|
if (supports_cool_ && supports_heat_) {
|
||||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||||
} else if (this->supports_cool_) {
|
} else if (supports_cool_) {
|
||||||
this->mode = climate::CLIMATE_MODE_COOL;
|
this->mode = climate::CLIMATE_MODE_COOL;
|
||||||
} else if (this->supports_heat_) {
|
} else if (supports_heat_) {
|
||||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||||
}
|
}
|
||||||
this->change_away_(false);
|
this->change_away_(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::control(const climate::ClimateCall &call) {
|
void BangBangClimate::control(const climate::ClimateCall &call) {
|
||||||
if (call.get_mode().has_value()) {
|
if (call.get_mode().has_value())
|
||||||
this->mode = *call.get_mode();
|
this->mode = *call.get_mode();
|
||||||
}
|
if (call.get_target_temperature_low().has_value())
|
||||||
if (call.get_target_temperature_low().has_value()) {
|
|
||||||
this->target_temperature_low = *call.get_target_temperature_low();
|
this->target_temperature_low = *call.get_target_temperature_low();
|
||||||
}
|
if (call.get_target_temperature_high().has_value())
|
||||||
if (call.get_target_temperature_high().has_value()) {
|
|
||||||
this->target_temperature_high = *call.get_target_temperature_high();
|
this->target_temperature_high = *call.get_target_temperature_high();
|
||||||
}
|
if (call.get_preset().has_value())
|
||||||
if (call.get_preset().has_value()) {
|
|
||||||
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
|
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
|
||||||
}
|
|
||||||
|
|
||||||
this->compute_state_();
|
this->compute_state_();
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
climate::ClimateTraits BangBangClimate::traits() {
|
climate::ClimateTraits BangBangClimate::traits() {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
|
traits.set_supports_current_temperature(true);
|
||||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
|
if (this->humidity_sensor_ != nullptr)
|
||||||
if (this->humidity_sensor_ != nullptr) {
|
traits.set_supports_current_humidity(true);
|
||||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
|
||||||
}
|
|
||||||
traits.set_supported_modes({
|
traits.set_supported_modes({
|
||||||
climate::CLIMATE_MODE_OFF,
|
climate::CLIMATE_MODE_OFF,
|
||||||
});
|
});
|
||||||
if (this->supports_cool_) {
|
if (supports_cool_)
|
||||||
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
|
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
|
||||||
}
|
if (supports_heat_)
|
||||||
if (this->supports_heat_) {
|
|
||||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
||||||
}
|
if (supports_cool_ && supports_heat_)
|
||||||
if (this->supports_cool_ && this->supports_heat_) {
|
|
||||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
|
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
|
||||||
}
|
traits.set_supports_two_point_target_temperature(true);
|
||||||
if (this->supports_away_) {
|
if (supports_away_) {
|
||||||
traits.set_supported_presets({
|
traits.set_supported_presets({
|
||||||
climate::CLIMATE_PRESET_HOME,
|
climate::CLIMATE_PRESET_HOME,
|
||||||
climate::CLIMATE_PRESET_AWAY,
|
climate::CLIMATE_PRESET_AWAY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
traits.set_supports_action(true);
|
||||||
return traits;
|
return traits;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::compute_state_() {
|
void BangBangClimate::compute_state_() {
|
||||||
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
||||||
this->switch_to_action_(climate::CLIMATE_ACTION_OFF);
|
this->switch_to_action_(climate::CLIMATE_ACTION_OFF);
|
||||||
@@ -135,7 +122,6 @@ void BangBangClimate::compute_state_() {
|
|||||||
|
|
||||||
this->switch_to_action_(target_action);
|
this->switch_to_action_(target_action);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
|
void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
|
||||||
if (action == this->action) {
|
if (action == this->action) {
|
||||||
// already in target mode
|
// already in target mode
|
||||||
@@ -180,7 +166,6 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
|
|||||||
this->prev_trigger_ = trig;
|
this->prev_trigger_ = trig;
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::change_away_(bool away) {
|
void BangBangClimate::change_away_(bool away) {
|
||||||
if (!away) {
|
if (!away) {
|
||||||
this->target_temperature_low = this->normal_config_.default_temperature_low;
|
this->target_temperature_low = this->normal_config_.default_temperature_low;
|
||||||
@@ -191,26 +176,22 @@ void BangBangClimate::change_away_(bool away) {
|
|||||||
}
|
}
|
||||||
this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
|
this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) {
|
void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) {
|
||||||
this->normal_config_ = normal_config;
|
this->normal_config_ = normal_config;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) {
|
void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) {
|
||||||
this->supports_away_ = true;
|
this->supports_away_ = true;
|
||||||
this->away_config_ = away_config;
|
this->away_config_ = away_config;
|
||||||
}
|
}
|
||||||
|
BangBangClimate::BangBangClimate()
|
||||||
|
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
|
||||||
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
||||||
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||||
|
|
||||||
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
|
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
|
||||||
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
|
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
|
||||||
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
|
|
||||||
|
|
||||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||||
|
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
|
||||||
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||||
|
|
||||||
void BangBangClimate::dump_config() {
|
void BangBangClimate::dump_config() {
|
||||||
LOG_CLIMATE("", "Bang Bang Climate", this);
|
LOG_CLIMATE("", "Bang Bang Climate", this);
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
|
|||||||
@@ -25,15 +25,14 @@ class BangBangClimate : public climate::Climate, public Component {
|
|||||||
|
|
||||||
void set_sensor(sensor::Sensor *sensor);
|
void set_sensor(sensor::Sensor *sensor);
|
||||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
|
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
|
||||||
|
Trigger<> *get_idle_trigger() const;
|
||||||
|
Trigger<> *get_cool_trigger() const;
|
||||||
void set_supports_cool(bool supports_cool);
|
void set_supports_cool(bool supports_cool);
|
||||||
|
Trigger<> *get_heat_trigger() const;
|
||||||
void set_supports_heat(bool supports_heat);
|
void set_supports_heat(bool supports_heat);
|
||||||
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
||||||
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
|
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
|
||||||
|
|
||||||
Trigger<> *get_idle_trigger() const;
|
|
||||||
Trigger<> *get_cool_trigger() const;
|
|
||||||
Trigger<> *get_heat_trigger() const;
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/// Override control to change settings of the climate device.
|
/// Override control to change settings of the climate device.
|
||||||
void control(const climate::ClimateCall &call) override;
|
void control(const climate::ClimateCall &call) override;
|
||||||
@@ -57,10 +56,16 @@ class BangBangClimate : public climate::Climate, public Component {
|
|||||||
*
|
*
|
||||||
* In idle mode, the controller is assumed to have both heating and cooling disabled.
|
* In idle mode, the controller is assumed to have both heating and cooling disabled.
|
||||||
*/
|
*/
|
||||||
Trigger<> *idle_trigger_{nullptr};
|
Trigger<> *idle_trigger_;
|
||||||
/** The trigger to call when the controller should switch to cooling mode.
|
/** The trigger to call when the controller should switch to cooling mode.
|
||||||
*/
|
*/
|
||||||
Trigger<> *cool_trigger_{nullptr};
|
Trigger<> *cool_trigger_;
|
||||||
|
/** Whether the controller supports cooling.
|
||||||
|
*
|
||||||
|
* A false value for this attribute means that the controller has no cooling action
|
||||||
|
* (for example a thermostat, where only heating and not-heating is possible).
|
||||||
|
*/
|
||||||
|
bool supports_cool_{false};
|
||||||
/** The trigger to call when the controller should switch to heating mode.
|
/** The trigger to call when the controller should switch to heating mode.
|
||||||
*
|
*
|
||||||
* A null value for this attribute means that the controller has no heating action
|
* A null value for this attribute means that the controller has no heating action
|
||||||
@@ -68,23 +73,15 @@ class BangBangClimate : public climate::Climate, public Component {
|
|||||||
* (blinds open) is possible.
|
* (blinds open) is possible.
|
||||||
*/
|
*/
|
||||||
Trigger<> *heat_trigger_{nullptr};
|
Trigger<> *heat_trigger_{nullptr};
|
||||||
|
bool supports_heat_{false};
|
||||||
/** A reference to the trigger that was previously active.
|
/** A reference to the trigger that was previously active.
|
||||||
*
|
*
|
||||||
* This is so that the previous trigger can be stopped before enabling a new one.
|
* This is so that the previous trigger can be stopped before enabling a new one.
|
||||||
*/
|
*/
|
||||||
Trigger<> *prev_trigger_{nullptr};
|
Trigger<> *prev_trigger_{nullptr};
|
||||||
|
|
||||||
/** Whether the controller supports cooling/heating
|
|
||||||
*
|
|
||||||
* A false value for this attribute means that the controller has no respective action
|
|
||||||
* (for example a thermostat, where only heating and not-heating is possible).
|
|
||||||
*/
|
|
||||||
bool supports_cool_{false};
|
|
||||||
bool supports_heat_{false};
|
|
||||||
|
|
||||||
bool supports_away_{false};
|
|
||||||
|
|
||||||
BangBangClimateTargetTempConfig normal_config_{};
|
BangBangClimateTargetTempConfig normal_config_{};
|
||||||
|
bool supports_away_{false};
|
||||||
BangBangClimateTargetTempConfig away_config_{};
|
BangBangClimateTargetTempConfig away_config_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ enum BedjetCommand : uint8_t {
|
|||||||
|
|
||||||
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
|
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
|
||||||
|
|
||||||
static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
|
static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
|
||||||
|
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
|
||||||
|
static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_;
|
||||||
|
|
||||||
} // namespace bedjet
|
} // namespace bedjet
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ namespace bedjet {
|
|||||||
|
|
||||||
using namespace esphome::climate;
|
using namespace esphome::climate;
|
||||||
|
|
||||||
static const char *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
|
static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
|
||||||
if (fan_step < BEDJET_FAN_SPEED_COUNT)
|
if (fan_step < BEDJET_FAN_SPEED_COUNT)
|
||||||
return BEDJET_FAN_STEP_NAMES[fan_step];
|
return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step];
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint8_t bedjet_fan_speed_to_step(const char *fan_step_percent) {
|
static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
|
||||||
for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) {
|
for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) {
|
||||||
if (strcmp(BEDJET_FAN_STEP_NAMES[i], fan_step_percent) == 0) {
|
if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ void BedJetClimate::dump_config() {
|
|||||||
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
|
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
|
||||||
}
|
}
|
||||||
for (const auto &mode : traits.get_supported_custom_fan_modes()) {
|
for (const auto &mode : traits.get_supported_custom_fan_modes()) {
|
||||||
ESP_LOGCONFIG(TAG, " - %s (c)", mode);
|
ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGCONFIG(TAG, " Supported presets:");
|
ESP_LOGCONFIG(TAG, " Supported presets:");
|
||||||
@@ -56,7 +56,7 @@ void BedJetClimate::dump_config() {
|
|||||||
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
|
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
|
||||||
}
|
}
|
||||||
for (const auto &preset : traits.get_supported_custom_presets()) {
|
for (const auto &preset : traits.get_supported_custom_presets()) {
|
||||||
ESP_LOGCONFIG(TAG, " - %s (c)", preset);
|
ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ void BedJetClimate::reset_state_() {
|
|||||||
this->target_temperature = NAN;
|
this->target_temperature = NAN;
|
||||||
this->current_temperature = NAN;
|
this->current_temperature = NAN;
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
this->clear_custom_preset_();
|
this->custom_preset.reset();
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ void BedJetClimate::control(const ClimateCall &call) {
|
|||||||
if (button_result) {
|
if (button_result) {
|
||||||
this->mode = mode;
|
this->mode = mode;
|
||||||
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
|
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
|
||||||
this->clear_custom_preset_();
|
this->custom_preset.reset();
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,8 @@ void BedJetClimate::control(const ClimateCall &call) {
|
|||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
this->mode = CLIMATE_MODE_HEAT;
|
this->mode = CLIMATE_MODE_HEAT;
|
||||||
this->set_preset_(CLIMATE_PRESET_BOOST);
|
this->preset = CLIMATE_PRESET_BOOST;
|
||||||
|
this->custom_preset.reset();
|
||||||
}
|
}
|
||||||
} else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
|
} else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
|
||||||
if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
|
if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
|
||||||
@@ -152,7 +153,7 @@ void BedJetClimate::control(const ClimateCall &call) {
|
|||||||
result = this->parent_->send_button(heat_button(this->heating_mode_));
|
result = this->parent_->send_button(heat_button(this->heating_mode_));
|
||||||
if (result) {
|
if (result) {
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
this->clear_custom_preset_();
|
this->custom_preset.reset();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
|
ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
|
||||||
@@ -163,27 +164,28 @@ void BedJetClimate::control(const ClimateCall &call) {
|
|||||||
ESP_LOGW(TAG, "Unsupported preset: %d", preset);
|
ESP_LOGW(TAG, "Unsupported preset: %d", preset);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (call.has_custom_preset()) {
|
} else if (call.get_custom_preset().has_value()) {
|
||||||
const char *preset = call.get_custom_preset();
|
std::string preset = *call.get_custom_preset();
|
||||||
bool result;
|
bool result;
|
||||||
|
|
||||||
if (strcmp(preset, "M1") == 0) {
|
if (preset == "M1") {
|
||||||
result = this->parent_->button_memory1();
|
result = this->parent_->button_memory1();
|
||||||
} else if (strcmp(preset, "M2") == 0) {
|
} else if (preset == "M2") {
|
||||||
result = this->parent_->button_memory2();
|
result = this->parent_->button_memory2();
|
||||||
} else if (strcmp(preset, "M3") == 0) {
|
} else if (preset == "M3") {
|
||||||
result = this->parent_->button_memory3();
|
result = this->parent_->button_memory3();
|
||||||
} else if (strcmp(preset, "LTD HT") == 0) {
|
} else if (preset == "LTD HT") {
|
||||||
result = this->parent_->button_heat();
|
result = this->parent_->button_heat();
|
||||||
} else if (strcmp(preset, "EXT HT") == 0) {
|
} else if (preset == "EXT HT") {
|
||||||
result = this->parent_->button_ext_heat();
|
result = this->parent_->button_ext_heat();
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW(TAG, "Unsupported preset: %s", preset);
|
ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
this->set_custom_preset_(preset);
|
this->custom_preset = preset;
|
||||||
|
this->preset.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,16 +207,19 @@ void BedJetClimate::control(const ClimateCall &call) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
this->set_fan_mode_(fan_mode);
|
this->fan_mode = fan_mode;
|
||||||
|
this->custom_fan_mode.reset();
|
||||||
}
|
}
|
||||||
} else if (call.has_custom_fan_mode()) {
|
} else if (call.get_custom_fan_mode().has_value()) {
|
||||||
const char *fan_mode = call.get_custom_fan_mode();
|
auto fan_mode = *call.get_custom_fan_mode();
|
||||||
auto fan_index = bedjet_fan_speed_to_step(fan_mode);
|
auto fan_index = bedjet_fan_speed_to_step(fan_mode);
|
||||||
if (fan_index <= 19) {
|
if (fan_index <= 19) {
|
||||||
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index);
|
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
|
||||||
|
fan_index);
|
||||||
bool result = this->parent_->set_fan_index(fan_index);
|
bool result = this->parent_->set_fan_index(fan_index);
|
||||||
if (result) {
|
if (result) {
|
||||||
this->set_custom_fan_mode_(fan_mode);
|
this->custom_fan_mode = fan_mode;
|
||||||
|
this->fan_mode.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,7 +245,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
|||||||
|
|
||||||
const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
|
const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
|
||||||
if (fan_mode_name != nullptr) {
|
if (fan_mode_name != nullptr) {
|
||||||
this->set_custom_fan_mode_(fan_mode_name);
|
this->custom_fan_mode = *fan_mode_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
|
// TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
|
||||||
@@ -250,7 +255,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
|||||||
this->mode = CLIMATE_MODE_OFF;
|
this->mode = CLIMATE_MODE_OFF;
|
||||||
this->action = CLIMATE_ACTION_IDLE;
|
this->action = CLIMATE_ACTION_IDLE;
|
||||||
this->fan_mode = CLIMATE_FAN_OFF;
|
this->fan_mode = CLIMATE_FAN_OFF;
|
||||||
this->clear_custom_preset_();
|
this->custom_preset.reset();
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -261,7 +266,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
|||||||
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
||||||
this->set_custom_preset_("LTD HT");
|
this->set_custom_preset_("LTD HT");
|
||||||
} else {
|
} else {
|
||||||
this->clear_custom_preset_();
|
this->custom_preset.reset();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -270,7 +275,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
|||||||
this->action = CLIMATE_ACTION_HEATING;
|
this->action = CLIMATE_ACTION_HEATING;
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
||||||
this->clear_custom_preset_();
|
this->custom_preset.reset();
|
||||||
} else {
|
} else {
|
||||||
this->set_custom_preset_("EXT HT");
|
this->set_custom_preset_("EXT HT");
|
||||||
}
|
}
|
||||||
@@ -279,19 +284,20 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
|||||||
case MODE_COOL:
|
case MODE_COOL:
|
||||||
this->mode = CLIMATE_MODE_FAN_ONLY;
|
this->mode = CLIMATE_MODE_FAN_ONLY;
|
||||||
this->action = CLIMATE_ACTION_COOLING;
|
this->action = CLIMATE_ACTION_COOLING;
|
||||||
this->clear_custom_preset_();
|
this->custom_preset.reset();
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MODE_DRY:
|
case MODE_DRY:
|
||||||
this->mode = CLIMATE_MODE_DRY;
|
this->mode = CLIMATE_MODE_DRY;
|
||||||
this->action = CLIMATE_ACTION_DRYING;
|
this->action = CLIMATE_ACTION_DRYING;
|
||||||
this->clear_custom_preset_();
|
this->custom_preset.reset();
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MODE_TURBO:
|
case MODE_TURBO:
|
||||||
this->set_preset_(CLIMATE_PRESET_BOOST);
|
this->preset = CLIMATE_PRESET_BOOST;
|
||||||
|
this->custom_preset.reset();
|
||||||
this->mode = CLIMATE_MODE_HEAT;
|
this->mode = CLIMATE_MODE_HEAT;
|
||||||
this->action = CLIMATE_ACTION_HEATING;
|
this->action = CLIMATE_ACTION_HEATING;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
|
|||||||
|
|
||||||
climate::ClimateTraits traits() override {
|
climate::ClimateTraits traits() override {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
traits.set_supports_action(true);
|
||||||
|
traits.set_supports_current_temperature(true);
|
||||||
traits.set_supported_modes({
|
traits.set_supported_modes({
|
||||||
climate::CLIMATE_MODE_OFF,
|
climate::CLIMATE_MODE_OFF,
|
||||||
climate::CLIMATE_MODE_HEAT,
|
climate::CLIMATE_MODE_HEAT,
|
||||||
@@ -43,20 +44,28 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
|
|||||||
});
|
});
|
||||||
|
|
||||||
// It would be better if we had a slider for the fan modes.
|
// It would be better if we had a slider for the fan modes.
|
||||||
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
|
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET);
|
||||||
traits.set_supported_presets({
|
traits.set_supported_presets({
|
||||||
// If we support NONE, then have to decide what happens if the user switches to it (turn off?)
|
// If we support NONE, then have to decide what happens if the user switches to it (turn off?)
|
||||||
// climate::CLIMATE_PRESET_NONE,
|
// climate::CLIMATE_PRESET_NONE,
|
||||||
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
|
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
|
||||||
climate::CLIMATE_PRESET_BOOST,
|
climate::CLIMATE_PRESET_BOOST,
|
||||||
});
|
});
|
||||||
// String literals are stored in rodata and valid for program lifetime
|
|
||||||
traits.set_supported_custom_presets({
|
traits.set_supported_custom_presets({
|
||||||
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
|
// We could fetch biodata from bedjet and set these names that way.
|
||||||
|
// But then we have to invert the lookup in order to send the right preset.
|
||||||
|
// For now, we can leave them as M1-3 to match the remote buttons.
|
||||||
|
// EXT HT added to match remote button.
|
||||||
|
"EXT HT",
|
||||||
"M1",
|
"M1",
|
||||||
"M2",
|
"M2",
|
||||||
"M3",
|
"M3",
|
||||||
});
|
});
|
||||||
|
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
||||||
|
traits.add_supported_custom_preset("LTD HT");
|
||||||
|
} else {
|
||||||
|
traits.add_supported_custom_preset("EXT HT");
|
||||||
|
}
|
||||||
traits.set_visual_min_temperature(19.0);
|
traits.set_visual_min_temperature(19.0);
|
||||||
traits.set_visual_max_temperature(43.0);
|
traits.set_visual_max_temperature(43.0);
|
||||||
traits.set_visual_temperature_step(1.0);
|
traits.set_visual_temperature_step(1.0);
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
#include "esphome/core/log.h"
|
|
||||||
#include "bh1900nux.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace bh1900nux {
|
|
||||||
|
|
||||||
static const char *const TAG = "bh1900nux.sensor";
|
|
||||||
|
|
||||||
// I2C Registers
|
|
||||||
static const uint8_t TEMPERATURE_REG = 0x00;
|
|
||||||
static const uint8_t CONFIG_REG = 0x01; // Not used and supported yet
|
|
||||||
static const uint8_t TEMPERATURE_LOW_REG = 0x02; // Not used and supported yet
|
|
||||||
static const uint8_t TEMPERATURE_HIGH_REG = 0x03; // Not used and supported yet
|
|
||||||
static const uint8_t SOFT_RESET_REG = 0x04;
|
|
||||||
|
|
||||||
// I2C Command payloads
|
|
||||||
static const uint8_t SOFT_RESET_PAYLOAD = 0x01; // Soft Reset value
|
|
||||||
|
|
||||||
static const float SENSOR_RESOLUTION = 0.0625f; // Sensor resolution per bit in degrees celsius
|
|
||||||
|
|
||||||
void BH1900NUXSensor::setup() {
|
|
||||||
// Initialize I2C device
|
|
||||||
i2c::ErrorCode result_code =
|
|
||||||
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
|
|
||||||
if (result_code != i2c::ERROR_OK) {
|
|
||||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BH1900NUXSensor::update() {
|
|
||||||
uint8_t temperature_raw[2];
|
|
||||||
if (this->read_register(TEMPERATURE_REG, temperature_raw, 2) != i2c::ERROR_OK) {
|
|
||||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combined raw value, unsigned and unaligned 16 bit
|
|
||||||
// Temperature is represented in just 12 bits, shift needed
|
|
||||||
int16_t raw_temperature_register_value = encode_uint16(temperature_raw[0], temperature_raw[1]);
|
|
||||||
raw_temperature_register_value >>= 4;
|
|
||||||
float temperature_value = raw_temperature_register_value * SENSOR_RESOLUTION; // Apply sensor resolution
|
|
||||||
|
|
||||||
this->publish_state(temperature_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BH1900NUXSensor::dump_config() {
|
|
||||||
LOG_SENSOR("", "BH1900NUX", this);
|
|
||||||
LOG_I2C_DEVICE(this);
|
|
||||||
LOG_UPDATE_INTERVAL(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace bh1900nux
|
|
||||||
} // namespace esphome
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
|
||||||
#include "esphome/components/sensor/sensor.h"
|
|
||||||
#include "esphome/components/i2c/i2c.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace bh1900nux {
|
|
||||||
|
|
||||||
class BH1900NUXSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
|
|
||||||
public:
|
|
||||||
void setup() override;
|
|
||||||
void update() override;
|
|
||||||
void dump_config() override;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace bh1900nux
|
|
||||||
} // namespace esphome
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import esphome.codegen as cg
|
|
||||||
from esphome.components import i2c, sensor
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.const import (
|
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
|
||||||
STATE_CLASS_MEASUREMENT,
|
|
||||||
UNIT_CELSIUS,
|
|
||||||
)
|
|
||||||
|
|
||||||
DEPENDENCIES = ["i2c"]
|
|
||||||
CODEOWNERS = ["@B48D81EFCC"]
|
|
||||||
|
|
||||||
sensor_ns = cg.esphome_ns.namespace("bh1900nux")
|
|
||||||
BH1900NUXSensor = sensor_ns.class_(
|
|
||||||
"BH1900NUXSensor", cg.PollingComponent, i2c.I2CDevice
|
|
||||||
)
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = (
|
|
||||||
sensor.sensor_schema(
|
|
||||||
BH1900NUXSensor,
|
|
||||||
accuracy_decimals=1,
|
|
||||||
unit_of_measurement=UNIT_CELSIUS,
|
|
||||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
)
|
|
||||||
.extend(cv.polling_component_schema("60s"))
|
|
||||||
.extend(i2c.i2c_device_schema(0x48))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
|
||||||
var = await sensor.new_sensor(config)
|
|
||||||
await cg.register_component(var, config)
|
|
||||||
await i2c.register_i2c_device(var, config)
|
|
||||||
@@ -155,7 +155,6 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
|
|||||||
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
|
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
|
||||||
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
|
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
|
||||||
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
||||||
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
|
|
||||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
|
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
|
||||||
|
|
||||||
_LOGGER = getLogger(__name__)
|
_LOGGER = getLogger(__name__)
|
||||||
@@ -265,31 +264,20 @@ async def delayed_off_filter_to_code(config, filter_id):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def autorepeat_filter_to_code(config, filter_id):
|
async def autorepeat_filter_to_code(config, filter_id):
|
||||||
|
timings = []
|
||||||
if len(config) > 0:
|
if len(config) > 0:
|
||||||
timings = [
|
timings.extend(
|
||||||
cg.StructInitializer(
|
(conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])
|
||||||
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
|
||||||
("delay", conf[CONF_DELAY]),
|
|
||||||
("time_off", conf[CONF_TIME_OFF]),
|
|
||||||
("time_on", conf[CONF_TIME_ON]),
|
|
||||||
)
|
|
||||||
for conf in config
|
for conf in config
|
||||||
]
|
)
|
||||||
else:
|
else:
|
||||||
timings = [
|
timings.append(
|
||||||
cg.StructInitializer(
|
(
|
||||||
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds,
|
||||||
("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds),
|
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
|
||||||
(
|
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
|
||||||
"time_off",
|
|
||||||
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"time_on",
|
|
||||||
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
]
|
)
|
||||||
var = cg.new_Pvariable(filter_id, timings)
|
var = cg.new_Pvariable(filter_id, timings)
|
||||||
await cg.register_component(var, {})
|
await cg.register_component(var, {})
|
||||||
return var
|
return var
|
||||||
@@ -300,7 +288,7 @@ async def lambda_filter_to_code(config, filter_id):
|
|||||||
lambda_ = await cg.process_lambda(
|
lambda_ = await cg.process_lambda(
|
||||||
config, [(bool, "x")], return_type=cg.optional.template(bool)
|
config, [(bool, "x")], return_type=cg.optional.template(bool)
|
||||||
)
|
)
|
||||||
return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
|
return cg.new_Pvariable(filter_id, lambda_)
|
||||||
|
|
||||||
|
|
||||||
@register_filter(
|
@register_filter(
|
||||||
@@ -548,6 +536,11 @@ def binary_sensor_schema(
|
|||||||
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
||||||
|
|
||||||
|
|
||||||
|
# Remove before 2025.11.0
|
||||||
|
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
|
||||||
|
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
|
||||||
|
|
||||||
|
|
||||||
async def setup_binary_sensor_core_(var, config):
|
async def setup_binary_sensor_core_(var, config):
|
||||||
await setup_entity(var, config, "binary_sensor")
|
await setup_entity(var, config, "binary_sensor")
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/helpers.h"
|
|
||||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@@ -92,8 +92,8 @@ class DoubleClickTrigger : public Trigger<> {
|
|||||||
|
|
||||||
class MultiClickTrigger : public Trigger<>, public Component {
|
class MultiClickTrigger : public Trigger<>, public Component {
|
||||||
public:
|
public:
|
||||||
explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing)
|
explicit MultiClickTrigger(BinarySensor *parent, std::vector<MultiClickTriggerEvent> timing)
|
||||||
: parent_(parent), timing_(timing) {}
|
: parent_(parent), timing_(std::move(timing)) {}
|
||||||
|
|
||||||
void setup() override {
|
void setup() override {
|
||||||
this->last_state_ = this->parent_->get_state_default(false);
|
this->last_state_ = this->parent_->get_state_default(false);
|
||||||
@@ -115,7 +115,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
|
|||||||
void trigger_();
|
void trigger_();
|
||||||
|
|
||||||
BinarySensor *parent_;
|
BinarySensor *parent_;
|
||||||
FixedVector<MultiClickTriggerEvent> timing_;
|
std::vector<MultiClickTriggerEvent> timing_;
|
||||||
uint32_t invalid_cooldown_{1000};
|
uint32_t invalid_cooldown_{1000};
|
||||||
optional<size_t> at_index_{};
|
optional<size_t> at_index_{};
|
||||||
bool last_state_{false};
|
bool last_state_{false};
|
||||||
@@ -141,7 +141,7 @@ class StateChangeTrigger : public Trigger<optional<bool>, optional<bool> > {
|
|||||||
template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> {
|
template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> {
|
||||||
public:
|
public:
|
||||||
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
|
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
|
||||||
bool check(const Ts &...x) override { return this->parent_->state == this->state_; }
|
bool check(Ts... x) override { return this->parent_->state == this->state_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
BinarySensor *parent_;
|
BinarySensor *parent_;
|
||||||
@@ -153,7 +153,7 @@ template<typename... Ts> class BinarySensorPublishAction : public Action<Ts...>
|
|||||||
explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {}
|
explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {}
|
||||||
TEMPLATABLE_VALUE(bool, state)
|
TEMPLATABLE_VALUE(bool, state)
|
||||||
|
|
||||||
void play(const Ts &...x) override {
|
void play(Ts... x) override {
|
||||||
auto val = this->state_.value(x...);
|
auto val = this->state_.value(x...);
|
||||||
this->sensor_->publish_state(val);
|
this->sensor_->publish_state(val);
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@ template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts..
|
|||||||
public:
|
public:
|
||||||
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
|
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
|
||||||
|
|
||||||
void play(const Ts &...x) override { this->sensor_->invalidate_state(); }
|
void play(Ts... x) override { this->sensor_->invalidate_state(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
BinarySensor *sensor_;
|
BinarySensor *sensor_;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#include "binary_sensor.h"
|
#include "binary_sensor.h"
|
||||||
#include "esphome/core/defines.h"
|
|
||||||
#include "esphome/core/controller_registry.h"
|
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@@ -39,9 +37,6 @@ void BinarySensor::send_state_internal(bool new_state) {
|
|||||||
// Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed
|
// Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed
|
||||||
if (this->set_state_(new_state)) {
|
if (this->set_state_(new_state)) {
|
||||||
ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state));
|
ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state));
|
||||||
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
|
|
||||||
ControllerRegistry::notify_binary_sensor_update(this);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +51,7 @@ void BinarySensor::add_filter(Filter *filter) {
|
|||||||
last_filter->next_ = filter;
|
last_filter->next_ = filter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
|
void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
|
||||||
for (Filter *filter : filters) {
|
for (Filter *filter : filters) {
|
||||||
this->add_filter(filter);
|
this->add_filter(filter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/components/binary_sensor/filter.h"
|
#include "esphome/components/binary_sensor/filter.h"
|
||||||
|
|
||||||
#include <initializer_list>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl
|
|||||||
void publish_initial_state(bool new_state);
|
void publish_initial_state(bool new_state);
|
||||||
|
|
||||||
void add_filter(Filter *filter);
|
void add_filter(Filter *filter);
|
||||||
void add_filters(std::initializer_list<Filter *> filters);
|
void add_filters(const std::vector<Filter *> &filters);
|
||||||
|
|
||||||
// ========== INTERNAL METHODS ==========
|
// ========== INTERNAL METHODS ==========
|
||||||
// (In most use cases you won't need these)
|
// (In most use cases you won't need these)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "filter.h"
|
#include "filter.h"
|
||||||
|
|
||||||
#include "binary_sensor.h"
|
#include "binary_sensor.h"
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD
|
|||||||
|
|
||||||
optional<bool> InvertFilter::new_value(bool value) { return !value; }
|
optional<bool> InvertFilter::new_value(bool value) { return !value; }
|
||||||
|
|
||||||
AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {}
|
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
|
||||||
|
|
||||||
optional<bool> AutorepeatFilter::new_value(bool value) {
|
optional<bool> AutorepeatFilter::new_value(bool value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
namespace binary_sensor {
|
namespace binary_sensor {
|
||||||
@@ -80,6 +82,11 @@ class InvertFilter : public Filter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct AutorepeatFilterTiming {
|
struct AutorepeatFilterTiming {
|
||||||
|
AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) {
|
||||||
|
this->delay = delay;
|
||||||
|
this->time_off = off;
|
||||||
|
this->time_on = on;
|
||||||
|
}
|
||||||
uint32_t delay;
|
uint32_t delay;
|
||||||
uint32_t time_off;
|
uint32_t time_off;
|
||||||
uint32_t time_on;
|
uint32_t time_on;
|
||||||
@@ -87,7 +94,7 @@ struct AutorepeatFilterTiming {
|
|||||||
|
|
||||||
class AutorepeatFilter : public Filter, public Component {
|
class AutorepeatFilter : public Filter, public Component {
|
||||||
public:
|
public:
|
||||||
explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings);
|
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
|
||||||
|
|
||||||
optional<bool> new_value(bool value) override;
|
optional<bool> new_value(bool value) override;
|
||||||
|
|
||||||
@@ -97,7 +104,7 @@ class AutorepeatFilter : public Filter, public Component {
|
|||||||
void next_timing_();
|
void next_timing_();
|
||||||
void next_value_(bool val);
|
void next_value_(bool val);
|
||||||
|
|
||||||
FixedVector<AutorepeatFilterTiming> timings_;
|
std::vector<AutorepeatFilterTiming> timings_;
|
||||||
uint8_t active_timing_{0};
|
uint8_t active_timing_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,21 +118,6 @@ class LambdaFilter : public Filter {
|
|||||||
std::function<optional<bool>(bool)> f_;
|
std::function<optional<bool>(bool)> f_;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Optimized lambda filter for stateless lambdas (no capture).
|
|
||||||
*
|
|
||||||
* Uses function pointer instead of std::function to reduce memory overhead.
|
|
||||||
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
|
|
||||||
*/
|
|
||||||
class StatelessLambdaFilter : public Filter {
|
|
||||||
public:
|
|
||||||
explicit StatelessLambdaFilter(optional<bool> (*f)(bool)) : f_(f) {}
|
|
||||||
|
|
||||||
optional<bool> new_value(bool value) override { return this->f_(value); }
|
|
||||||
|
|
||||||
protected:
|
|
||||||
optional<bool> (*f_)(bool);
|
|
||||||
};
|
|
||||||
|
|
||||||
class SettleFilter : public Filter, public Component {
|
class SettleFilter : public Filter, public Component {
|
||||||
public:
|
public:
|
||||||
optional<bool> new_value(bool value) override;
|
optional<bool> new_value(bool value) override;
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ void BL0906::handle_actions_() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ActionCallbackFuncPtr ptr_func = nullptr;
|
ActionCallbackFuncPtr ptr_func = nullptr;
|
||||||
for (size_t i = 0; i < this->action_queue_.size(); i++) {
|
for (int i = 0; i < this->action_queue_.size(); i++) {
|
||||||
ptr_func = this->action_queue_[i];
|
ptr_func = this->action_queue_[i];
|
||||||
if (ptr_func) {
|
if (ptr_func) {
|
||||||
ESP_LOGI(TAG, "HandleActionCallback[%zu]", i);
|
ESP_LOGI(TAG, "HandleActionCallback[%d]", i);
|
||||||
(this->*ptr_func)();
|
(this->*ptr_func)();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class BL0906 : public PollingComponent, public uart::UARTDevice {
|
|||||||
|
|
||||||
template<typename... Ts> class ResetEnergyAction : public Action<Ts...>, public Parented<BL0906> {
|
template<typename... Ts> class ResetEnergyAction : public Action<Ts...>, public Parented<BL0906> {
|
||||||
public:
|
public:
|
||||||
void play(const Ts &...x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); }
|
void play(Ts... x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); }
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace bl0906
|
} // namespace bl0906
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number";
|
|||||||
void CalibrationNumber::setup() {
|
void CalibrationNumber::setup() {
|
||||||
float value = 0.0f;
|
float value = 0.0f;
|
||||||
if (this->restore_value_) {
|
if (this->restore_value_) {
|
||||||
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
|
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
|
||||||
if (!this->pref_.load(&value)) {
|
if (!this->pref_.load(&value)) {
|
||||||
value = 0.0f;
|
value = 0.0f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ void BL0942::loop() {
|
|||||||
if (!avail) {
|
if (!avail) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (static_cast<size_t>(avail) < sizeof(buffer)) {
|
if (avail < sizeof(buffer)) {
|
||||||
if (!this->rx_start_) {
|
if (!this->rx_start_) {
|
||||||
this->rx_start_ = millis();
|
this->rx_start_ = millis();
|
||||||
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
|
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
|
||||||
@@ -148,7 +148,7 @@ void BL0942::setup() {
|
|||||||
|
|
||||||
this->write_reg_(BL0942_REG_USR_WRPROT, 0);
|
this->write_reg_(BL0942_REG_USR_WRPROT, 0);
|
||||||
|
|
||||||
if (static_cast<uint32_t>(this->read_reg_(BL0942_REG_MODE)) != mode)
|
if (this->read_reg_(BL0942_REG_MODE) != mode)
|
||||||
this->status_set_warning(LOG_STR("BL0942 setup failed!"));
|
this->status_set_warning(LOG_STR("BL0942 setup failed!"));
|
||||||
|
|
||||||
this->flush();
|
this->flush();
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from esphome.const import (
|
|||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
CONF_VALUE,
|
CONF_VALUE,
|
||||||
)
|
)
|
||||||
from esphome.core import ID
|
|
||||||
|
|
||||||
AUTO_LOAD = ["esp32_ble_client"]
|
AUTO_LOAD = ["esp32_ble_client"]
|
||||||
CODEOWNERS = ["@buxtronix", "@clydebarrow"]
|
CODEOWNERS = ["@buxtronix", "@clydebarrow"]
|
||||||
@@ -199,12 +198,7 @@ async def ble_write_to_code(config, action_id, template_arg, args):
|
|||||||
templ = await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
|
templ = await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
|
||||||
cg.add(var.set_value_template(templ))
|
cg.add(var.set_value_template(templ))
|
||||||
else:
|
else:
|
||||||
# Generate static array in flash to avoid RAM copy
|
cg.add(var.set_value_simple(value))
|
||||||
if isinstance(value, bytes):
|
|
||||||
value = list(value)
|
|
||||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
|
||||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*value))
|
|
||||||
cg.add(var.set_value_simple(arr, len(value)))
|
|
||||||
|
|
||||||
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
|
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
|
||||||
cg.add(
|
cg.add(
|
||||||
|
|||||||
@@ -106,30 +106,22 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
|||||||
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||||
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||||
|
|
||||||
void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
|
void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) {
|
||||||
this->value_.func = func;
|
this->value_template_ = std::move(func);
|
||||||
this->len_ = -1; // Sentinel value indicates template mode
|
has_simple_value_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store pointer to static data in flash (no RAM copy)
|
void set_value_simple(const std::vector<uint8_t> &value) {
|
||||||
void set_value_simple(const uint8_t *data, size_t len) {
|
this->value_simple_ = value;
|
||||||
this->value_.data = data;
|
has_simple_value_ = true;
|
||||||
this->len_ = len; // Length >= 0 indicates static mode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void play(const Ts &...x) override {}
|
void play(Ts... x) override {}
|
||||||
|
|
||||||
void play_complex(const Ts &...x) override {
|
void play_complex(Ts... x) override {
|
||||||
this->num_running_++;
|
this->num_running_++;
|
||||||
this->var_ = std::make_tuple(x...);
|
this->var_ = std::make_tuple(x...);
|
||||||
std::vector<uint8_t> value;
|
auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...);
|
||||||
if (this->len_ >= 0) {
|
|
||||||
// Static mode: copy from flash to vector
|
|
||||||
value.assign(this->value_.data, this->value_.data + this->len_);
|
|
||||||
} else {
|
|
||||||
// Template mode: call function
|
|
||||||
value = this->value_.func(x...);
|
|
||||||
}
|
|
||||||
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
||||||
if (!write(value))
|
if (!write(value))
|
||||||
this->play_next_(x...);
|
this->play_next_(x...);
|
||||||
@@ -203,11 +195,9 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
BLEClient *ble_client_;
|
BLEClient *ble_client_;
|
||||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
bool has_simple_value_ = true;
|
||||||
union Value {
|
std::vector<uint8_t> value_simple_;
|
||||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
std::function<std::vector<uint8_t>(Ts...)> value_template_{};
|
||||||
const uint8_t *data; // Pointer to static data in flash
|
|
||||||
} value_;
|
|
||||||
espbt::ESPBTUUID service_uuid_;
|
espbt::ESPBTUUID service_uuid_;
|
||||||
espbt::ESPBTUUID char_uuid_;
|
espbt::ESPBTUUID char_uuid_;
|
||||||
std::tuple<Ts...> var_{};
|
std::tuple<Ts...> var_{};
|
||||||
@@ -220,12 +210,12 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
|
|||||||
public:
|
public:
|
||||||
BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
|
BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
|
||||||
|
|
||||||
void play(const Ts &...x) override {
|
void play(Ts... x) override {
|
||||||
uint32_t passkey;
|
uint32_t passkey;
|
||||||
if (has_simple_value_) {
|
if (has_simple_value_) {
|
||||||
passkey = this->value_.simple;
|
passkey = this->value_simple_;
|
||||||
} else {
|
} else {
|
||||||
passkey = this->value_.template_func(x...);
|
passkey = this->value_template_(x...);
|
||||||
}
|
}
|
||||||
if (passkey > 999999)
|
if (passkey > 999999)
|
||||||
return;
|
return;
|
||||||
@@ -234,63 +224,59 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
|
|||||||
esp_ble_passkey_reply(remote_bda, true, passkey);
|
esp_ble_passkey_reply(remote_bda, true, passkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_value_template(uint32_t (*func)(Ts...)) {
|
void set_value_template(std::function<uint32_t(Ts...)> func) {
|
||||||
this->value_.template_func = func;
|
this->value_template_ = std::move(func);
|
||||||
this->has_simple_value_ = false;
|
has_simple_value_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_value_simple(const uint32_t &value) {
|
void set_value_simple(const uint32_t &value) {
|
||||||
this->value_.simple = value;
|
this->value_simple_ = value;
|
||||||
this->has_simple_value_ = true;
|
has_simple_value_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
BLEClient *parent_{nullptr};
|
BLEClient *parent_{nullptr};
|
||||||
bool has_simple_value_ = true;
|
bool has_simple_value_ = true;
|
||||||
union {
|
uint32_t value_simple_{0};
|
||||||
uint32_t simple;
|
std::function<uint32_t(Ts...)> value_template_{};
|
||||||
uint32_t (*template_func)(Ts...);
|
|
||||||
} value_{.simple = 0};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> {
|
template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> {
|
||||||
public:
|
public:
|
||||||
BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
|
BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
|
||||||
|
|
||||||
void play(const Ts &...x) override {
|
void play(Ts... x) override {
|
||||||
esp_bd_addr_t remote_bda;
|
esp_bd_addr_t remote_bda;
|
||||||
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
|
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
|
||||||
if (has_simple_value_) {
|
if (has_simple_value_) {
|
||||||
esp_ble_confirm_reply(remote_bda, this->value_.simple);
|
esp_ble_confirm_reply(remote_bda, this->value_simple_);
|
||||||
} else {
|
} else {
|
||||||
esp_ble_confirm_reply(remote_bda, this->value_.template_func(x...));
|
esp_ble_confirm_reply(remote_bda, this->value_template_(x...));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_value_template(bool (*func)(Ts...)) {
|
void set_value_template(std::function<bool(Ts...)> func) {
|
||||||
this->value_.template_func = func;
|
this->value_template_ = std::move(func);
|
||||||
this->has_simple_value_ = false;
|
has_simple_value_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_value_simple(const bool &value) {
|
void set_value_simple(const bool &value) {
|
||||||
this->value_.simple = value;
|
this->value_simple_ = value;
|
||||||
this->has_simple_value_ = true;
|
has_simple_value_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
BLEClient *parent_{nullptr};
|
BLEClient *parent_{nullptr};
|
||||||
bool has_simple_value_ = true;
|
bool has_simple_value_ = true;
|
||||||
union {
|
bool value_simple_{false};
|
||||||
bool simple;
|
std::function<bool(Ts...)> value_template_{};
|
||||||
bool (*template_func)(Ts...);
|
|
||||||
} value_{.simple = false};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> {
|
template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> {
|
||||||
public:
|
public:
|
||||||
BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; }
|
BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; }
|
||||||
|
|
||||||
void play(const Ts &...x) override {
|
void play(Ts... x) override {
|
||||||
esp_bd_addr_t remote_bda;
|
esp_bd_addr_t remote_bda;
|
||||||
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
|
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
|
||||||
esp_ble_remove_bond_device(remote_bda);
|
esp_ble_remove_bond_device(remote_bda);
|
||||||
@@ -325,9 +311,9 @@ template<typename... Ts> class BLEClientConnectAction : public Action<Ts...>, pu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// not used since we override play_complex_
|
// not used since we override play_complex_
|
||||||
void play(const Ts &...x) override {}
|
void play(Ts... x) override {}
|
||||||
|
|
||||||
void play_complex(const Ts &...x) override {
|
void play_complex(Ts... x) override {
|
||||||
// it makes no sense to have multiple instances of this running at the same time.
|
// it makes no sense to have multiple instances of this running at the same time.
|
||||||
// this would occur only if the same automation was re-triggered while still
|
// this would occur only if the same automation was re-triggered while still
|
||||||
// running. So just cancel the second chain if this is detected.
|
// running. So just cancel the second chain if this is detected.
|
||||||
@@ -370,9 +356,9 @@ template<typename... Ts> class BLEClientDisconnectAction : public Action<Ts...>,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// not used since we override play_complex_
|
// not used since we override play_complex_
|
||||||
void play(const Ts &...x) override {}
|
void play(Ts... x) override {}
|
||||||
|
|
||||||
void play_complex(const Ts &...x) override {
|
void play_complex(Ts... x) override {
|
||||||
this->num_running_++;
|
this->num_running_++;
|
||||||
if (this->node_state == espbt::ClientState::IDLE) {
|
if (this->node_state == espbt::ClientState::IDLE) {
|
||||||
this->play_next_(x...);
|
this->play_next_(x...);
|
||||||
|
|||||||
@@ -77,9 +77,6 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||||
// For non-notify characteristics, trigger an immediate read after service discovery
|
|
||||||
// to avoid peripherals disconnecting due to inactivity
|
|
||||||
this->update();
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -120,9 +117,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
|||||||
}
|
}
|
||||||
|
|
||||||
float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
|
float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
|
||||||
if (this->has_data_to_value_) {
|
if (this->data_to_value_func_.has_value()) {
|
||||||
std::vector<uint8_t> data(value, value + value_len);
|
std::vector<uint8_t> data(value, value + value_len);
|
||||||
return this->data_to_value_func_(data);
|
return (*this->data_to_value_func_)(data);
|
||||||
} else {
|
} else {
|
||||||
return value[0];
|
return value[0];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ namespace ble_client {
|
|||||||
|
|
||||||
namespace espbt = esphome::esp32_ble_tracker;
|
namespace espbt = esphome::esp32_ble_tracker;
|
||||||
|
|
||||||
|
using data_to_value_t = std::function<float(std::vector<uint8_t>)>;
|
||||||
|
|
||||||
class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode {
|
class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode {
|
||||||
public:
|
public:
|
||||||
void loop() override;
|
void loop() override;
|
||||||
@@ -31,17 +33,13 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
|
|||||||
void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||||
void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||||
void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||||
void set_data_to_value(float (*lambda)(const std::vector<uint8_t> &)) {
|
void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; }
|
||||||
this->data_to_value_func_ = lambda;
|
|
||||||
this->has_data_to_value_ = true;
|
|
||||||
}
|
|
||||||
void set_enable_notify(bool notify) { this->notify_ = notify; }
|
void set_enable_notify(bool notify) { this->notify_ = notify; }
|
||||||
uint16_t handle;
|
uint16_t handle;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
float parse_data_(uint8_t *value, uint16_t value_len);
|
float parse_data_(uint8_t *value, uint16_t value_len);
|
||||||
bool has_data_to_value_{false};
|
optional<data_to_value_t> data_to_value_func_{};
|
||||||
float (*data_to_value_func_)(const std::vector<uint8_t> &){};
|
|
||||||
bool notify_;
|
bool notify_;
|
||||||
espbt::ESPBTUUID service_uuid_;
|
espbt::ESPBTUUID service_uuid_;
|
||||||
espbt::ESPBTUUID char_uuid_;
|
espbt::ESPBTUUID char_uuid_;
|
||||||
|
|||||||
@@ -79,9 +79,6 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||||
// For non-notify characteristics, trigger an immediate read after service discovery
|
|
||||||
// to avoid peripherals disconnecting due to inactivity
|
|
||||||
this->update();
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import esphome.codegen as cg
|
|
||||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE
|
|
||||||
|
|
||||||
AUTO_LOAD = ["zephyr_ble_server"]
|
|
||||||
CODEOWNERS = ["@tomaszduda23"]
|
|
||||||
|
|
||||||
ble_nus_ns = cg.esphome_ns.namespace("ble_nus")
|
|
||||||
BLENUS = ble_nus_ns.class_("BLENUS", cg.Component)
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
|
||||||
cv.Schema(
|
|
||||||
{
|
|
||||||
cv.GenerateID(): cv.declare_id(BLENUS),
|
|
||||||
cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of(
|
|
||||||
*[CONF_LOGS], lower=True
|
|
||||||
),
|
|
||||||
}
|
|
||||||
).extend(cv.COMPONENT_SCHEMA),
|
|
||||||
cv.only_with_framework("zephyr"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
|
||||||
zephyr_add_prj_conf("BT_NUS", True)
|
|
||||||
cg.add(var.set_expose_log(config[CONF_TYPE] == CONF_LOGS))
|
|
||||||
await cg.register_component(var, config)
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
#ifdef USE_ZEPHYR
|
|
||||||
#include "ble_nus.h"
|
|
||||||
#include <zephyr/kernel.h>
|
|
||||||
#include <bluetooth/services/nus.h>
|
|
||||||
#include "esphome/core/log.h"
|
|
||||||
#ifdef USE_LOGGER
|
|
||||||
#include "esphome/components/logger/logger.h"
|
|
||||||
#include "esphome/core/application.h"
|
|
||||||
#endif
|
|
||||||
#include <zephyr/sys/ring_buffer.h>
|
|
||||||
|
|
||||||
namespace esphome::ble_nus {
|
|
||||||
|
|
||||||
constexpr size_t BLE_TX_BUF_SIZE = 2048;
|
|
||||||
|
|
||||||
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
|
|
||||||
BLENUS *global_ble_nus;
|
|
||||||
RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE);
|
|
||||||
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
|
|
||||||
|
|
||||||
static const char *const TAG = "ble_nus";
|
|
||||||
|
|
||||||
size_t BLENUS::write_array(const uint8_t *data, size_t len) {
|
|
||||||
if (atomic_get(&this->tx_status_) == TX_DISABLED) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return ring_buf_put(&global_ble_tx_ring_buf, data, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BLENUS::connected(bt_conn *conn, uint8_t err) {
|
|
||||||
if (err == 0) {
|
|
||||||
global_ble_nus->conn_.store(bt_conn_ref(conn));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BLENUS::disconnected(bt_conn *conn, uint8_t reason) {
|
|
||||||
if (global_ble_nus->conn_) {
|
|
||||||
bt_conn_unref(global_ble_nus->conn_.load());
|
|
||||||
// Connection array is global static.
|
|
||||||
// Reference can be kept even if disconnected.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BLENUS::tx_callback(bt_conn *conn) {
|
|
||||||
atomic_cas(&global_ble_nus->tx_status_, TX_BUSY, TX_ENABLED);
|
|
||||||
ESP_LOGVV(TAG, "Sent operation completed");
|
|
||||||
}
|
|
||||||
|
|
||||||
void BLENUS::send_enabled_callback(bt_nus_send_status status) {
|
|
||||||
switch (status) {
|
|
||||||
case BT_NUS_SEND_STATUS_ENABLED:
|
|
||||||
atomic_set(&global_ble_nus->tx_status_, TX_ENABLED);
|
|
||||||
#ifdef USE_LOGGER
|
|
||||||
if (global_ble_nus->expose_log_) {
|
|
||||||
App.schedule_dump_config();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
ESP_LOGD(TAG, "NUS notification has been enabled");
|
|
||||||
break;
|
|
||||||
case BT_NUS_SEND_STATUS_DISABLED:
|
|
||||||
atomic_set(&global_ble_nus->tx_status_, TX_DISABLED);
|
|
||||||
ESP_LOGD(TAG, "NUS notification has been disabled");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) {
|
|
||||||
ESP_LOGD(TAG, "Received %d bytes.", len);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BLENUS::setup() {
|
|
||||||
bt_nus_cb callbacks = {
|
|
||||||
.received = rx_callback,
|
|
||||||
.sent = tx_callback,
|
|
||||||
.send_enabled = send_enabled_callback,
|
|
||||||
};
|
|
||||||
|
|
||||||
bt_nus_init(&callbacks);
|
|
||||||
|
|
||||||
static bt_conn_cb conn_callbacks = {
|
|
||||||
.connected = BLENUS::connected,
|
|
||||||
.disconnected = BLENUS::disconnected,
|
|
||||||
};
|
|
||||||
|
|
||||||
bt_conn_cb_register(&conn_callbacks);
|
|
||||||
|
|
||||||
global_ble_nus = this;
|
|
||||||
#ifdef USE_LOGGER
|
|
||||||
if (logger::global_logger != nullptr && this->expose_log_) {
|
|
||||||
logger::global_logger->add_on_log_callback(
|
|
||||||
[this](int level, const char *tag, const char *message, size_t message_len) {
|
|
||||||
this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
|
|
||||||
const char c = '\n';
|
|
||||||
this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void BLENUS::dump_config() {
|
|
||||||
ESP_LOGCONFIG(TAG, "ble nus:");
|
|
||||||
ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_));
|
|
||||||
uint32_t mtu = 0;
|
|
||||||
bt_conn *conn = this->conn_.load();
|
|
||||||
if (conn) {
|
|
||||||
mtu = bt_nus_get_mtu(conn);
|
|
||||||
}
|
|
||||||
ESP_LOGCONFIG(TAG, " MTU: %u", mtu);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BLENUS::loop() {
|
|
||||||
if (ring_buf_is_empty(&global_ble_tx_ring_buf)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!atomic_cas(&this->tx_status_, TX_ENABLED, TX_BUSY)) {
|
|
||||||
if (atomic_get(&this->tx_status_) == TX_DISABLED) {
|
|
||||||
ring_buf_reset(&global_ble_tx_ring_buf);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bt_conn *conn = this->conn_.load();
|
|
||||||
if (conn) {
|
|
||||||
conn = bt_conn_ref(conn);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nullptr == conn) {
|
|
||||||
atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t req_len = bt_nus_get_mtu(conn);
|
|
||||||
|
|
||||||
uint8_t *buf;
|
|
||||||
uint32_t size = ring_buf_get_claim(&global_ble_tx_ring_buf, &buf, req_len);
|
|
||||||
|
|
||||||
int err, err2;
|
|
||||||
|
|
||||||
err = bt_nus_send(conn, buf, size);
|
|
||||||
err2 = ring_buf_get_finish(&global_ble_tx_ring_buf, size);
|
|
||||||
if (err2) {
|
|
||||||
// It should no happen.
|
|
||||||
ESP_LOGE(TAG, "Size %u exceeds valid bytes in the ring buffer (%d error)", size, err2);
|
|
||||||
}
|
|
||||||
if (err == 0) {
|
|
||||||
ESP_LOGVV(TAG, "Sent %d bytes", size);
|
|
||||||
} else {
|
|
||||||
ESP_LOGE(TAG, "Failed to send %d bytes (%d error)", size, err);
|
|
||||||
atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
|
|
||||||
}
|
|
||||||
bt_conn_unref(conn);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace esphome::ble_nus
|
|
||||||
#endif
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#ifdef USE_ZEPHYR
|
|
||||||
#include "esphome/core/defines.h"
|
|
||||||
#include "esphome/core/component.h"
|
|
||||||
#include <shell/shell_bt_nus.h>
|
|
||||||
#include <atomic>
|
|
||||||
|
|
||||||
namespace esphome::ble_nus {
|
|
||||||
|
|
||||||
class BLENUS : public Component {
|
|
||||||
enum TxStatus {
|
|
||||||
TX_DISABLED,
|
|
||||||
TX_ENABLED,
|
|
||||||
TX_BUSY,
|
|
||||||
};
|
|
||||||
|
|
||||||
public:
|
|
||||||
void setup() override;
|
|
||||||
void dump_config() override;
|
|
||||||
void loop() override;
|
|
||||||
size_t write_array(const uint8_t *data, size_t len);
|
|
||||||
void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
|
|
||||||
|
|
||||||
protected:
|
|
||||||
static void send_enabled_callback(bt_nus_send_status status);
|
|
||||||
static void tx_callback(bt_conn *conn);
|
|
||||||
static void rx_callback(bt_conn *conn, const uint8_t *data, uint16_t len);
|
|
||||||
static void connected(bt_conn *conn, uint8_t err);
|
|
||||||
static void disconnected(bt_conn *conn, uint8_t reason);
|
|
||||||
|
|
||||||
std::atomic<bt_conn *> conn_ = nullptr;
|
|
||||||
bool expose_log_ = false;
|
|
||||||
atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace esphome::ble_nus
|
|
||||||
#endif
|
|
||||||
@@ -230,8 +230,8 @@ void BluetoothConnection::send_service_for_discovery_() {
|
|||||||
service_resp.handle = service_result.start_handle;
|
service_resp.handle = service_result.start_handle;
|
||||||
|
|
||||||
if (total_char_count > 0) {
|
if (total_char_count > 0) {
|
||||||
// Initialize FixedVector with exact count and process characteristics
|
// Reserve space and process characteristics
|
||||||
service_resp.characteristics.init(total_char_count);
|
service_resp.characteristics.reserve(total_char_count);
|
||||||
uint16_t char_offset = 0;
|
uint16_t char_offset = 0;
|
||||||
esp_gattc_char_elem_t char_result;
|
esp_gattc_char_elem_t char_result;
|
||||||
while (true) { // characteristics
|
while (true) { // characteristics
|
||||||
@@ -253,7 +253,9 @@ void BluetoothConnection::send_service_for_discovery_() {
|
|||||||
|
|
||||||
service_resp.characteristics.emplace_back();
|
service_resp.characteristics.emplace_back();
|
||||||
auto &characteristic_resp = service_resp.characteristics.back();
|
auto &characteristic_resp = service_resp.characteristics.back();
|
||||||
|
|
||||||
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids);
|
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids);
|
||||||
|
|
||||||
characteristic_resp.handle = char_result.char_handle;
|
characteristic_resp.handle = char_result.char_handle;
|
||||||
characteristic_resp.properties = char_result.properties;
|
characteristic_resp.properties = char_result.properties;
|
||||||
char_offset++;
|
char_offset++;
|
||||||
@@ -269,11 +271,12 @@ void BluetoothConnection::send_service_for_discovery_() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (total_desc_count == 0) {
|
if (total_desc_count == 0) {
|
||||||
|
// No descriptors, continue to next characteristic
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize FixedVector with exact count and process descriptors
|
// Reserve space and process descriptors
|
||||||
characteristic_resp.descriptors.init(total_desc_count);
|
characteristic_resp.descriptors.reserve(total_desc_count);
|
||||||
uint16_t desc_offset = 0;
|
uint16_t desc_offset = 0;
|
||||||
esp_gattc_descr_elem_t desc_result;
|
esp_gattc_descr_elem_t desc_result;
|
||||||
while (true) { // descriptors
|
while (true) { // descriptors
|
||||||
@@ -294,7 +297,9 @@ void BluetoothConnection::send_service_for_discovery_() {
|
|||||||
|
|
||||||
characteristic_resp.descriptors.emplace_back();
|
characteristic_resp.descriptors.emplace_back();
|
||||||
auto &descriptor_resp = characteristic_resp.descriptors.back();
|
auto &descriptor_resp = characteristic_resp.descriptors.back();
|
||||||
|
|
||||||
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids);
|
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids);
|
||||||
|
|
||||||
descriptor_resp.handle = desc_result.handle;
|
descriptor_resp.handle = desc_result.handle;
|
||||||
desc_offset++;
|
desc_offset++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,12 +155,16 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
|
|||||||
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
|
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
|
||||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||||
auto *connection = this->connections_[i];
|
auto *connection = this->connections_[i];
|
||||||
uint64_t conn_addr = connection->get_address();
|
if (connection->get_address() == address)
|
||||||
|
|
||||||
if (conn_addr == address)
|
|
||||||
return connection;
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
if (reserve && conn_addr == 0) {
|
if (!reserve)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||||
|
auto *connection = this->connections_[i];
|
||||||
|
if (connection->get_address() == 0) {
|
||||||
connection->send_service_ = INIT_SENDING_SERVICES;
|
connection->send_service_ = INIT_SENDING_SERVICES;
|
||||||
connection->set_address(address);
|
connection->set_address(address);
|
||||||
// All connections must start at INIT
|
// All connections must start at INIT
|
||||||
@@ -171,6 +175,7 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
|
|||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,7 @@
|
|||||||
|
|
||||||
#include "bluetooth_connection.h"
|
#include "bluetooth_connection.h"
|
||||||
|
|
||||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
|
||||||
#include <esp_bt.h>
|
#include <esp_bt.h>
|
||||||
#endif
|
|
||||||
#include <esp_bt_device.h>
|
#include <esp_bt_device.h>
|
||||||
|
|
||||||
namespace esphome::bluetooth_proxy {
|
namespace esphome::bluetooth_proxy {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(BME680BSECComponent),
|
cv.GenerateID(): cv.declare_id(BME680BSECComponent),
|
||||||
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
|
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
|
||||||
cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum(
|
cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum(
|
||||||
IAQ_MODE_OPTIONS, upper=True
|
IAQ_MODE_OPTIONS, upper=True
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ CONFIG_SCHEMA_BASE = (
|
|||||||
cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum(
|
cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum(
|
||||||
VOLTAGE_OPTIONS, upper=True
|
VOLTAGE_OPTIONS, upper=True
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
|
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_STATE_SAVE_INTERVAL, default="6hours"
|
CONF_STATE_SAVE_INTERVAL, default="6hours"
|
||||||
): cv.positive_time_period_minutes,
|
): cv.positive_time_period_minutes,
|
||||||
|
|||||||
@@ -84,6 +84,11 @@ def button_schema(
|
|||||||
return _BUTTON_SCHEMA.extend(schema)
|
return _BUTTON_SCHEMA.extend(schema)
|
||||||
|
|
||||||
|
|
||||||
|
# Remove before 2025.11.0
|
||||||
|
BUTTON_SCHEMA = button_schema(Button)
|
||||||
|
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
|
||||||
|
|
||||||
|
|
||||||
async def setup_button_core_(var, config):
|
async def setup_button_core_(var, config):
|
||||||
await setup_entity(var, config, "button")
|
await setup_entity(var, config, "button")
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ template<typename... Ts> class PressAction : public Action<Ts...> {
|
|||||||
public:
|
public:
|
||||||
explicit PressAction(Button *button) : button_(button) {}
|
explicit PressAction(Button *button) : button_(button) {}
|
||||||
|
|
||||||
void play(const Ts &...x) override { this->button_->press(); }
|
void play(Ts... x) override { this->button_->press(); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
Button *button_;
|
Button *button_;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from esphome import automation
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID
|
from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID
|
||||||
from esphome.core import CORE, ID
|
from esphome.core import CORE
|
||||||
|
|
||||||
CODEOWNERS = ["@mvturnho", "@danielschramm"]
|
CODEOWNERS = ["@mvturnho", "@danielschramm"]
|
||||||
IS_PLATFORM_COMPONENT = True
|
IS_PLATFORM_COMPONENT = True
|
||||||
@@ -176,8 +176,5 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
|
|||||||
else:
|
else:
|
||||||
if isinstance(data, bytes):
|
if isinstance(data, bytes):
|
||||||
data = [int(x) for x in data]
|
data = [int(x) for x in data]
|
||||||
# Generate static array in flash to avoid RAM copy
|
cg.add(var.set_data_static(data))
|
||||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
|
||||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
|
||||||
cg.add(var.set_data_static(arr, len(data)))
|
|
||||||
return var
|
return var
|
||||||
|
|||||||
@@ -105,23 +105,20 @@ class Canbus : public Component {
|
|||||||
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
|
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
|
||||||
callback_manager_{};
|
callback_manager_{};
|
||||||
|
|
||||||
virtual bool setup_internal() = 0;
|
virtual bool setup_internal();
|
||||||
virtual Error send_message(struct CanFrame *frame) = 0;
|
virtual Error send_message(struct CanFrame *frame);
|
||||||
virtual Error read_message(struct CanFrame *frame) = 0;
|
virtual Error read_message(struct CanFrame *frame);
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public Parented<Canbus> {
|
template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public Parented<Canbus> {
|
||||||
public:
|
public:
|
||||||
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
|
void set_data_template(const std::function<std::vector<uint8_t>(Ts...)> func) {
|
||||||
// Stateless lambdas (generated by ESPHome) implicitly convert to function pointers
|
this->data_func_ = func;
|
||||||
this->data_.func = func;
|
this->static_ = false;
|
||||||
this->len_ = -1; // Sentinel value indicates template mode
|
|
||||||
}
|
}
|
||||||
|
void set_data_static(const std::vector<uint8_t> &data) {
|
||||||
// Store pointer to static data in flash (no RAM copy)
|
this->data_static_ = data;
|
||||||
void set_data_static(const uint8_t *data, size_t len) {
|
this->static_ = true;
|
||||||
this->data_.data = data;
|
|
||||||
this->len_ = len; // Length >= 0 indicates static mode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
|
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
|
||||||
@@ -132,30 +129,25 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P
|
|||||||
this->remote_transmission_request_ = remote_transmission_request;
|
this->remote_transmission_request_ = remote_transmission_request;
|
||||||
}
|
}
|
||||||
|
|
||||||
void play(const Ts &...x) override {
|
void play(Ts... x) override {
|
||||||
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
|
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
|
||||||
auto use_extended_id =
|
auto use_extended_id =
|
||||||
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;
|
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;
|
||||||
std::vector<uint8_t> data;
|
if (this->static_) {
|
||||||
if (this->len_ >= 0) {
|
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, this->data_static_);
|
||||||
// Static mode: copy from flash to vector
|
|
||||||
data.assign(this->data_.data, this->data_.data + this->len_);
|
|
||||||
} else {
|
} else {
|
||||||
// Template mode: call function
|
auto val = this->data_func_(x...);
|
||||||
data = this->data_.func(x...);
|
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, val);
|
||||||
}
|
}
|
||||||
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
optional<uint32_t> can_id_{};
|
optional<uint32_t> can_id_{};
|
||||||
optional<bool> use_extended_id_{};
|
optional<bool> use_extended_id_{};
|
||||||
bool remote_transmission_request_{false};
|
bool remote_transmission_request_{false};
|
||||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
bool static_{false};
|
||||||
union Data {
|
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
std::vector<uint8_t> data_static_{};
|
||||||
const uint8_t *data; // Pointer to static data in flash
|
|
||||||
} data_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {
|
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {
|
||||||
|
|||||||
@@ -8,30 +8,17 @@ namespace cap1188 {
|
|||||||
static const char *const TAG = "cap1188";
|
static const char *const TAG = "cap1188";
|
||||||
|
|
||||||
void CAP1188Component::setup() {
|
void CAP1188Component::setup() {
|
||||||
this->disable_loop();
|
// Reset device using the reset pin
|
||||||
|
if (this->reset_pin_ != nullptr) {
|
||||||
// no reset pin
|
this->reset_pin_->setup();
|
||||||
if (this->reset_pin_ == nullptr) {
|
this->reset_pin_->digital_write(false);
|
||||||
this->finish_setup_();
|
delay(100); // NOLINT
|
||||||
return;
|
this->reset_pin_->digital_write(true);
|
||||||
|
delay(100); // NOLINT
|
||||||
|
this->reset_pin_->digital_write(false);
|
||||||
|
delay(100); // NOLINT
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset pin configured so reset before finishing setup
|
|
||||||
this->reset_pin_->setup();
|
|
||||||
this->reset_pin_->digital_write(false);
|
|
||||||
// delay after reset pin write
|
|
||||||
this->set_timeout(100, [this]() {
|
|
||||||
this->reset_pin_->digital_write(true);
|
|
||||||
// delay after reset pin write
|
|
||||||
this->set_timeout(100, [this]() {
|
|
||||||
this->reset_pin_->digital_write(false);
|
|
||||||
// delay after reset pin write
|
|
||||||
this->set_timeout(100, [this]() { this->finish_setup_(); });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void CAP1188Component::finish_setup_() {
|
|
||||||
// Check if CAP1188 is actually connected
|
// Check if CAP1188 is actually connected
|
||||||
this->read_byte(CAP1188_PRODUCT_ID, &this->cap1188_product_id_);
|
this->read_byte(CAP1188_PRODUCT_ID, &this->cap1188_product_id_);
|
||||||
this->read_byte(CAP1188_MANUFACTURE_ID, &this->cap1188_manufacture_id_);
|
this->read_byte(CAP1188_MANUFACTURE_ID, &this->cap1188_manufacture_id_);
|
||||||
@@ -57,9 +44,6 @@ void CAP1188Component::finish_setup_() {
|
|||||||
|
|
||||||
// Speed up a bit
|
// Speed up a bit
|
||||||
this->write_byte(CAP1188_STAND_BY_CONFIGURATION, 0x30);
|
this->write_byte(CAP1188_STAND_BY_CONFIGURATION, 0x30);
|
||||||
|
|
||||||
// Setup successful, so enable loop
|
|
||||||
this->enable_loop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CAP1188Component::dump_config() {
|
void CAP1188Component::dump_config() {
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ class CAP1188Component : public Component, public i2c::I2CDevice {
|
|||||||
void loop() override;
|
void loop() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void finish_setup_();
|
|
||||||
|
|
||||||
std::vector<CAP1188Channel *> channels_{};
|
std::vector<CAP1188Channel *> channels_{};
|
||||||
uint8_t touch_threshold_{0x20};
|
uint8_t touch_threshold_{0x20};
|
||||||
uint8_t allow_multiple_touches_{0x80};
|
uint8_t allow_multiple_touches_{0x80};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user