mirror of
https://github.com/esphome/esphome.git
synced 2025-11-09 19:41:49 +00:00
Compare commits
50 Commits
api_shrink
...
controller
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d648b3f462 | ||
|
|
05d7410afa | ||
|
|
32797534a7 | ||
|
|
b264c6caac | ||
|
|
e3fb074a60 | ||
|
|
6e7f66d393 | ||
|
|
ac85949f17 | ||
|
|
0962024d99 | ||
|
|
327543303c | ||
|
|
8229e3a471 | ||
|
|
1b6471f4b0 | ||
|
|
c87d07ba70 | ||
|
|
fc8dc33023 | ||
|
|
c0e4f415f1 | ||
|
|
871c5ddb4e | ||
|
|
6ef2763cab | ||
|
|
929279dc23 | ||
|
|
6fa0f1e290 | ||
|
|
51eb8ea1d0 | ||
|
|
cbdd663fbf | ||
|
|
c77bb3b269 | ||
|
|
f1009a7468 | ||
|
|
295fe8da04 | ||
|
|
79d1a558af | ||
|
|
a5bf55b6ac | ||
|
|
85d2565f25 | ||
|
|
4f08f0750a | ||
|
|
3c41e080c5 | ||
|
|
7c30d57391 | ||
|
|
182e106bfa | ||
|
|
d0b399d771 | ||
|
|
5d20e3a3b4 | ||
|
|
ba5fa7c10a | ||
|
|
5cdb891b58 | ||
|
|
26607713bb | ||
|
|
895d76ca03 | ||
|
|
74187845b7 | ||
|
|
822eacfd77 | ||
|
|
ab5d8f67ae | ||
|
|
83f30a64ed | ||
|
|
5eea7bdb44 | ||
|
|
bdfd88441a | ||
|
|
20b6e0d5c2 | ||
|
|
ce5e608863 | ||
|
|
aa5795c019 | ||
|
|
00c0854323 | ||
|
|
be006ecadd | ||
|
|
b08419fa47 | ||
|
|
d36ef050a9 | ||
|
|
df53ff7afe |
@@ -51,7 +51,79 @@ This document provides essential context for AI models interacting with this pro
|
||||
|
||||
* **Naming Conventions:**
|
||||
* **Python:** Follows PEP 8. Use clear, descriptive names following snake_case.
|
||||
* **C++:** Follows the Google C++ Style Guide.
|
||||
* **C++:** Follows the Google C++ Style Guide with these specifics (following clang-tidy conventions):
|
||||
- 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:**
|
||||
* **Standard Files:**
|
||||
@@ -368,3 +440,45 @@ 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`.
|
||||
* **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.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.3
|
||||
rev: v0.14.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -290,6 +290,7 @@ esphome/components/mcp23x17_base/* @jesserockz
|
||||
esphome/components/mcp23xxx_base/* @jesserockz
|
||||
esphome/components/mcp2515/* @danielschramm @mvturnho
|
||||
esphome/components/mcp3204/* @rsumner
|
||||
esphome/components/mcp3221/* @philippderdiedas
|
||||
esphome/components/mcp4461/* @p1ngb4ck
|
||||
esphome/components/mcp4728/* @berfenger
|
||||
esphome/components/mcp47a1/* @jesserockz
|
||||
|
||||
309
SENSOR_CALLBACK_OPTIMIZATION_FINAL.md
Normal file
309
SENSOR_CALLBACK_OPTIMIZATION_FINAL.md
Normal file
@@ -0,0 +1,309 @@
|
||||
# Sensor Callback Optimization - Zero-Cost Implementation
|
||||
|
||||
## The Perfect Optimization
|
||||
|
||||
By storing the partition count **in the Sensor class** alongside existing small fields, we achieve a **zero-cost optimization** with only wins and no losses!
|
||||
|
||||
## Implementation Design
|
||||
|
||||
### Key Insight: Reuse Available Padding
|
||||
|
||||
Sensor already has grouped small fields with 1 byte of available space:
|
||||
|
||||
```cpp
|
||||
class Sensor {
|
||||
protected:
|
||||
// Existing small members grouped together
|
||||
int8_t accuracy_decimals_{-1}; // 1 byte
|
||||
StateClass state_class_{STATE_CLASS_NONE}; // 1 byte (uint8_t enum)
|
||||
|
||||
struct SensorFlags {
|
||||
uint8_t has_accuracy_override : 1;
|
||||
uint8_t has_state_class_override : 1;
|
||||
uint8_t force_update : 1;
|
||||
uint8_t reserved : 5;
|
||||
} sensor_flags_{}; // 1 byte
|
||||
|
||||
uint8_t filtered_count_{0}; // 1 byte ← NEW! Perfect fit!
|
||||
// Total: 4 bytes (naturally aligned, no padding waste)
|
||||
};
|
||||
```
|
||||
|
||||
### Callbacks Structure (Heap-Allocated)
|
||||
|
||||
```cpp
|
||||
class Sensor {
|
||||
protected:
|
||||
std::unique_ptr<std::vector<std::function<void(float)>>> callbacks_;
|
||||
|
||||
// Partition layout: [filtered_0, ..., filtered_n-1, raw_0, ..., raw_m-1]
|
||||
// ^ ^
|
||||
// 0 filtered_count_
|
||||
};
|
||||
```
|
||||
|
||||
### Core Methods
|
||||
|
||||
```cpp
|
||||
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
if (!this->callbacks_) {
|
||||
this->callbacks_ = std::make_unique<std::vector<std::function<void(float)>>>();
|
||||
}
|
||||
|
||||
// Add to filtered section: append + swap into position
|
||||
this->callbacks_->push_back(std::move(callback));
|
||||
if (this->filtered_count_ < this->callbacks_->size() - 1) {
|
||||
std::swap((*this->callbacks_)[this->filtered_count_],
|
||||
(*this->callbacks_)[this->callbacks_->size() - 1]);
|
||||
}
|
||||
this->filtered_count_++;
|
||||
}
|
||||
|
||||
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
|
||||
if (!this->callbacks_) {
|
||||
this->callbacks_ = std::make_unique<std::vector<std::function<void(float)>>>();
|
||||
}
|
||||
|
||||
// Add to raw section: just append (already at end)
|
||||
this->callbacks_->push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void Sensor::publish_state(float state) {
|
||||
this->raw_state = state;
|
||||
|
||||
// Call raw callbacks (before filters)
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = this->filtered_count_; i < this->callbacks_->size(); i++) {
|
||||
(*this->callbacks_)[i](state);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
|
||||
|
||||
// ... apply filters ...
|
||||
}
|
||||
|
||||
void Sensor::internal_send_state_to_frontend(float state) {
|
||||
this->set_has_state(true);
|
||||
this->state = state;
|
||||
|
||||
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy",
|
||||
this->get_name().c_str(), state, this->get_unit_of_measurement_ref().c_str(),
|
||||
this->get_accuracy_decimals());
|
||||
|
||||
// Call filtered callbacks (after filters)
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = 0; i < this->filtered_count_; i++) {
|
||||
(*this->callbacks_)[i](state);
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_sensor_update(this);
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
## Memory Analysis (ESP32 32-bit)
|
||||
|
||||
### Current Implementation
|
||||
```cpp
|
||||
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; // 4 bytes
|
||||
CallbackManager<void(float)> callback_; // 12 bytes
|
||||
```
|
||||
|
||||
### Partitioned Implementation
|
||||
```cpp
|
||||
std::unique_ptr<std::vector<std::function<void(float)>>> callbacks_; // 4 bytes
|
||||
uint8_t filtered_count_{0}; // 0 bytes (uses existing padding slot)
|
||||
```
|
||||
|
||||
## Memory Comparison
|
||||
|
||||
| Scenario | Current | Partitioned | Savings |
|
||||
|----------|---------|-------------|---------|
|
||||
| **No callbacks** | 16 bytes | 4 bytes | **+12 bytes** ✅ |
|
||||
| **1 filtered (MQTT)** | 32 bytes | 32 bytes | **±0 bytes** ✅ |
|
||||
| **1 raw only** | 44 bytes | 32 bytes | **+12 bytes** ✅ |
|
||||
| **1 raw + 1 filtered** | 60 bytes | 48 bytes | **+12 bytes** ✅ |
|
||||
| **2 filtered** | 48 bytes | 48 bytes | **±0 bytes** ✅ |
|
||||
|
||||
### Detailed Breakdown
|
||||
|
||||
**No callbacks:**
|
||||
- Current: 4 (raw ptr) + 12 (callback_ vec) = 16 bytes
|
||||
- Partitioned: 4 (callbacks_ ptr) + 0 (count uses existing padding) = **4 bytes**
|
||||
- **Saves: 12 bytes** ✅
|
||||
|
||||
**1 filtered callback (MQTT):**
|
||||
- Current: 4 + 12 + 16 (function) = 32 bytes
|
||||
- Partitioned: 4 (ptr) + 12 (vector on heap) + 16 (function) = **32 bytes**
|
||||
- **Saves: 0 bytes** (ZERO COST!) ✅
|
||||
|
||||
**1 raw + 1 filtered:**
|
||||
- Current: 4 + 12 + 12 (raw vec on heap) + 16 + 16 = 60 bytes
|
||||
- Partitioned: 4 + 12 + 16 + 16 = **48 bytes**
|
||||
- **Saves: 12 bytes** ✅
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
### Typical IoT Device (15 sensors)
|
||||
**API-only (no MQTT, no automations):**
|
||||
- Current: 15 × 16 = 240 bytes
|
||||
- Optimized: 15 × 4 = 60 bytes
|
||||
- **Saves: 180 bytes** ✅
|
||||
|
||||
**With MQTT on all sensors:**
|
||||
- Current: 15 × 32 = 480 bytes
|
||||
- Optimized: 15 × 32 = 480 bytes
|
||||
- **Saves: 0 bytes** (ZERO COST!) ✅
|
||||
|
||||
**Mixed (10 API-only + 5 MQTT):**
|
||||
- Current: (10 × 16) + (5 × 32) = 320 bytes
|
||||
- Optimized: (10 × 4) + (5 × 32) = 200 bytes
|
||||
- **Saves: 120 bytes** ✅
|
||||
|
||||
### Large Dashboard (50 sensors)
|
||||
**API-only:**
|
||||
- Current: 50 × 16 = 800 bytes
|
||||
- Optimized: 50 × 4 = 200 bytes
|
||||
- **Saves: 600 bytes** ✅
|
||||
|
||||
**With MQTT on 20 sensors:**
|
||||
- Current: (30 × 16) + (20 × 32) = 1,120 bytes
|
||||
- Optimized: (30 × 4) + (20 × 32) = 760 bytes
|
||||
- **Saves: 360 bytes** ✅
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Time Complexity
|
||||
- `add_on_state_callback()`: **O(1)** - append + swap
|
||||
- `add_on_raw_state_callback()`: **O(1)** - append
|
||||
- `publish_state()` (call raw): **O(m)** - iterate raw section
|
||||
- `internal_send_state_to_frontend()` (call filtered): **O(n)** - iterate filtered section
|
||||
|
||||
### Hot Path Performance
|
||||
**Before:**
|
||||
```cpp
|
||||
if (this->raw_callback_) {
|
||||
this->raw_callback_->call(state); // Separate container
|
||||
}
|
||||
// ...
|
||||
this->callback_.call(state); // Separate container
|
||||
```
|
||||
|
||||
**After:**
|
||||
```cpp
|
||||
// Call raw callbacks
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = filtered_count_; i < callbacks_->size(); i++) {
|
||||
(*callbacks_)[i](state);
|
||||
}
|
||||
}
|
||||
// ...
|
||||
// Call filtered callbacks
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = 0; i < filtered_count_; i++) {
|
||||
(*callbacks_)[i](state);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Performance impact:**
|
||||
- ✅ Better cache locality (single vector instead of two containers)
|
||||
- ✅ No branching inside loops (vs checking callback types)
|
||||
- ✅ Tight loops for typical 0-2 callbacks case
|
||||
- ⚠️ One extra nullptr check (negligible, likely free with branch prediction)
|
||||
|
||||
## Advantages
|
||||
|
||||
### Memory
|
||||
1. ✅ **12 bytes saved** per sensor without callbacks (most common after Controller Registry)
|
||||
2. ✅ **ZERO cost** for MQTT-enabled sensors (32 → 32 bytes)
|
||||
3. ✅ **12 bytes saved** for sensors with both raw + filtered callbacks
|
||||
4. ✅ **No padding waste** (reuses existing padding slot in Sensor class)
|
||||
|
||||
### Architecture
|
||||
1. ✅ **Cleaner:** ONE vector instead of TWO separate CallbackManager instances
|
||||
2. ✅ **Simpler:** Partitioned vector is more elegant than dual containers
|
||||
3. ✅ **Better cache locality:** Callbacks stored contiguously
|
||||
4. ✅ **O(1) insertion:** Both add operations use append (+ optional swap)
|
||||
|
||||
### Code Quality
|
||||
1. ✅ **No new fields in hot path:** filtered_count_ reuses padding
|
||||
2. ✅ **No branching in iteration:** Direct range iteration
|
||||
3. ✅ **Order preservation not needed:** Callbacks are independent
|
||||
|
||||
## Implementation Files
|
||||
|
||||
### Modified Files
|
||||
- `esphome/components/sensor/sensor.h`
|
||||
- `esphome/components/sensor/sensor.cpp`
|
||||
|
||||
### Changes Required
|
||||
1. Replace callback storage with partitioned vector
|
||||
2. Update `add_on_state_callback()` to use swap-based insertion
|
||||
3. Update `add_on_raw_state_callback()` to append
|
||||
4. Update `publish_state()` to iterate raw section
|
||||
5. Update `internal_send_state_to_frontend()` to iterate filtered section
|
||||
6. Add `filtered_count_` field (uses existing padding)
|
||||
|
||||
## TextSensor Implementation
|
||||
|
||||
TextSensor can use the **exact same pattern**:
|
||||
|
||||
```cpp
|
||||
class TextSensor {
|
||||
protected:
|
||||
std::unique_ptr<std::vector<std::function<void(std::string)>>> callbacks_;
|
||||
uint8_t filtered_count_{0}; // Store in class (check for available padding)
|
||||
};
|
||||
```
|
||||
|
||||
Same benefits apply!
|
||||
|
||||
## Migration Risk Assessment
|
||||
|
||||
### Low Risk
|
||||
- ✅ No API changes (public methods unchanged)
|
||||
- ✅ Callback behavior identical (same execution order within each type)
|
||||
- ✅ Only internal implementation changes
|
||||
- ✅ Well-tested pattern (partitioned vectors common in CS)
|
||||
|
||||
### Testing Strategy
|
||||
1. Unit tests: Verify callback execution order preserved
|
||||
2. Integration tests: Test with MQTT, automations, copy components
|
||||
3. Memory benchmarks: Confirm actual RAM savings on real devices
|
||||
4. Regression tests: Ensure no behavior changes for existing configs
|
||||
|
||||
## Recommendation
|
||||
|
||||
**IMPLEMENT IMMEDIATELY** ✅
|
||||
|
||||
This optimization has:
|
||||
- ✅ **Zero cost** for MQTT users (32 → 32 bytes)
|
||||
- ✅ **12-byte savings** for API-only sensors (most common)
|
||||
- ✅ **12-byte savings** for sensors with automations
|
||||
- ✅ **Better architecture** (one container vs two)
|
||||
- ✅ **No downsides** whatsoever
|
||||
|
||||
**Expected savings for typical device: 150-600 bytes**
|
||||
|
||||
This is a **pure win** optimization with no trade-offs!
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
### Phase 1: Sensor ⭐⭐⭐ (HIGHEST PRIORITY)
|
||||
- Most common entity type
|
||||
- Biggest impact
|
||||
- Zero cost even for MQTT users
|
||||
- **Start here!**
|
||||
|
||||
### Phase 2: TextSensor ⭐⭐
|
||||
- Second most common entity with raw callbacks
|
||||
- Same pattern as Sensor
|
||||
|
||||
### Phase 3: Other entities (simple lazy vector) ⭐
|
||||
- BinarySensor, Switch, etc. don't have raw callbacks
|
||||
- Can use simpler lazy-allocated vector
|
||||
- Still save 12 bytes when no callbacks
|
||||
845
callback_manager_optimize.md
Normal file
845
callback_manager_optimize.md
Normal file
@@ -0,0 +1,845 @@
|
||||
# CallbackManager Optimization Plan
|
||||
|
||||
**Note:** ESPHome uses C++20 (gnu++20), so implementations leverage modern C++ features:
|
||||
- **Concepts** for type constraints and better error messages
|
||||
- **Designated initializers** for cleaner struct initialization
|
||||
- **consteval** for compile-time validation
|
||||
- **Requires clauses** for inline constraints
|
||||
|
||||
## Current State
|
||||
|
||||
### Memory Profile (ESP32 - 32-bit)
|
||||
|
||||
```cpp
|
||||
sizeof(std::function<void(T)>): 32 bytes
|
||||
sizeof(void*): 4 bytes
|
||||
sizeof(function pointer): 4 bytes
|
||||
```
|
||||
|
||||
### Current Implementation
|
||||
|
||||
```cpp
|
||||
template<typename... Ts> class CallbackManager<void(Ts...)> {
|
||||
public:
|
||||
void add(std::function<void(Ts...)> &&callback) {
|
||||
this->callbacks_.push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto &cb : this->callbacks_)
|
||||
cb(args...);
|
||||
}
|
||||
|
||||
size_t size() const { return this->callbacks_.size(); }
|
||||
|
||||
protected:
|
||||
std::vector<std::function<void(Ts...)>> callbacks_;
|
||||
};
|
||||
```
|
||||
|
||||
### Memory Cost Per Instance
|
||||
|
||||
- **Per callback:** 32 bytes (std::function storage)
|
||||
- **Vector reallocation code:** ~132 bytes (`_M_realloc_append` template instantiation)
|
||||
- **Example (1 callback):** 32 + 132 = 164 bytes
|
||||
|
||||
### Codebase Usage
|
||||
|
||||
- **Total CallbackManager instances:** ~67 files
|
||||
- **Estimated total callbacks:** 100-150 across all components
|
||||
- **Examples:**
|
||||
- `sensor.h`: `CallbackManager<void(float)>` - multiple callbacks per sensor
|
||||
- `esp32_ble_tracker.h`: `CallbackManager<void(ScannerState)>` - 1 callback (bluetooth_proxy)
|
||||
- `esp32_improv.h`: `CallbackManager<void(State, Error)>` - up to 5 callbacks (automation triggers)
|
||||
- `climate.h`: `CallbackManager<void()>` - multiple callbacks for state/control
|
||||
|
||||
### Current Usage Pattern
|
||||
|
||||
All callbacks currently use lambda captures:
|
||||
|
||||
```cpp
|
||||
// bluetooth_proxy.cpp
|
||||
parent_->add_scanner_state_callback([this](ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
});
|
||||
|
||||
// sensor.cpp (via automation)
|
||||
sensor->add_on_state_callback([this](float state) {
|
||||
this->trigger(state);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optimization Options
|
||||
|
||||
### Option 1: Function Pointer + Context (Recommended)
|
||||
|
||||
**C++20 Implementation (Type-Safe with Concepts):**
|
||||
|
||||
```cpp
|
||||
#include <concepts>
|
||||
#include <type_traits>
|
||||
|
||||
// Concept to validate callback signature
|
||||
template<typename F, typename Context, typename... Ts>
|
||||
concept CallbackFunction = requires(F func, Context* ctx, Ts... args) {
|
||||
{ func(ctx, args...) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*invoker)(void*, Ts...); // 4 bytes - type-erased invoker
|
||||
void* context; // 4 bytes - captured context
|
||||
// Total: 8 bytes
|
||||
};
|
||||
|
||||
// Type-safe invoker template - knows real context type
|
||||
template<typename Context>
|
||||
static void invoke(void* ctx, Ts... args) {
|
||||
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(
|
||||
*static_cast<void**>(ctx)
|
||||
);
|
||||
auto typed_ctx = static_cast<Context*>(
|
||||
*reinterpret_cast<void**>(static_cast<char*>(ctx) + sizeof(void*))
|
||||
);
|
||||
typed_func(typed_ctx, args...);
|
||||
}
|
||||
|
||||
std::vector<Callback> callbacks_;
|
||||
|
||||
public:
|
||||
// Type-safe registration with concept constraint
|
||||
template<typename Context>
|
||||
requires CallbackFunction<void(*)(Context*, Ts...), Context, Ts...>
|
||||
void add(void (*func)(Context*, Ts...), Context* context) {
|
||||
// Use designated initializers (C++20)
|
||||
callbacks_.push_back({
|
||||
.invoker = [](void* storage, Ts... args) {
|
||||
// Extract function pointer and context from packed storage
|
||||
void* func_and_ctx[2];
|
||||
std::memcpy(func_and_ctx, storage, sizeof(func_and_ctx));
|
||||
|
||||
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(func_and_ctx[0]);
|
||||
auto typed_ctx = static_cast<Context*>(func_and_ctx[1]);
|
||||
typed_func(typed_ctx, args...);
|
||||
},
|
||||
.context = nullptr // Will store packed data
|
||||
});
|
||||
|
||||
// Pack function pointer and context into the callback storage
|
||||
void* func_and_ctx[2] = { reinterpret_cast<void*>(func), context };
|
||||
std::memcpy(&callbacks_.back(), func_and_ctx, sizeof(func_and_ctx));
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto& cb : callbacks_) {
|
||||
cb.invoker(&cb, args...);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr size_t size() const { return callbacks_.size(); }
|
||||
};
|
||||
```
|
||||
|
||||
**Cleaner C++20 Implementation (12 bytes, simpler):**
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*invoker)(void*, void*, Ts...); // 4 bytes - generic invoker
|
||||
void* func_ptr; // 4 bytes - actual function
|
||||
void* context; // 4 bytes - context
|
||||
// Total: 12 bytes (still 20 bytes saved vs std::function!)
|
||||
};
|
||||
|
||||
template<typename Context>
|
||||
static consteval auto make_invoker() {
|
||||
return +[](void* func, void* ctx, Ts... args) {
|
||||
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(func);
|
||||
typed_func(static_cast<Context*>(ctx), args...);
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<Callback> callbacks_;
|
||||
|
||||
public:
|
||||
// C++20 concepts for type safety
|
||||
template<typename Context>
|
||||
requires std::invocable<void(*)(Context*, Ts...), Context*, Ts...>
|
||||
void add(void (*func)(Context*, Ts...), Context* context) {
|
||||
// C++20 designated initializers
|
||||
callbacks_.push_back({
|
||||
.invoker = make_invoker<Context>(),
|
||||
.func_ptr = reinterpret_cast<void*>(func),
|
||||
.context = context
|
||||
});
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto& cb : callbacks_) {
|
||||
cb.invoker(cb.func_ptr, cb.context, args...);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr size_t size() const { return callbacks_.size(); }
|
||||
constexpr bool empty() const { return callbacks_.empty(); }
|
||||
};
|
||||
```
|
||||
|
||||
**Most Efficient C++20 Implementation (8 bytes):**
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*invoker)(void*, Ts...); // 4 bytes
|
||||
void* context; // 4 bytes
|
||||
// Total: 8 bytes - maximum savings!
|
||||
};
|
||||
|
||||
// C++20: consteval ensures compile-time evaluation
|
||||
template<typename Context>
|
||||
static consteval auto make_invoker() {
|
||||
// The + forces decay to function pointer
|
||||
return +[](void* ctx, Ts... args) {
|
||||
// Unpack the storage struct
|
||||
struct Storage {
|
||||
void (*func)(Context*, Ts...);
|
||||
Context* context;
|
||||
};
|
||||
auto* storage = static_cast<Storage*>(ctx);
|
||||
storage->func(storage->context, args...);
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<Callback> callbacks_;
|
||||
|
||||
public:
|
||||
template<typename Context>
|
||||
requires std::invocable<void(*)(Context*, Ts...), Context*, Ts...>
|
||||
void add(void (*func)(Context*, Ts...), Context* context) {
|
||||
// Allocate storage for function + context
|
||||
struct Storage {
|
||||
void (*func)(Context*, Ts...);
|
||||
Context* context;
|
||||
};
|
||||
|
||||
auto* storage = new Storage{func, context};
|
||||
|
||||
callbacks_.push_back({
|
||||
.invoker = make_invoker<Context>(),
|
||||
.context = storage
|
||||
});
|
||||
}
|
||||
|
||||
~CallbackManager() {
|
||||
// Clean up storage
|
||||
for (auto& cb : callbacks_) {
|
||||
delete static_cast<void*>(cb.context);
|
||||
}
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto& cb : callbacks_) {
|
||||
cb.invoker(cb.context, args...);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr size_t size() const { return callbacks_.size(); }
|
||||
};
|
||||
```
|
||||
|
||||
**Simplest C++20 Implementation (Recommended):**
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*invoker)(void*, void*, Ts...); // 4 bytes
|
||||
void* func_ptr; // 4 bytes
|
||||
void* context; // 4 bytes
|
||||
// Total: 12 bytes
|
||||
};
|
||||
|
||||
template<typename Context>
|
||||
static void invoke(void* func, void* ctx, Ts... args) {
|
||||
reinterpret_cast<void(*)(Context*, Ts...)>(func)(static_cast<Context*>(ctx), args...);
|
||||
}
|
||||
|
||||
std::vector<Callback> callbacks_;
|
||||
|
||||
public:
|
||||
template<typename Context>
|
||||
requires std::invocable<void(*)(Context*, Ts...), Context*, Ts...>
|
||||
void add(void (*func)(Context*, Ts...), Context* context) {
|
||||
callbacks_.push_back({
|
||||
.invoker = &invoke<Context>,
|
||||
.func_ptr = reinterpret_cast<void*>(func),
|
||||
.context = context
|
||||
});
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto& cb : callbacks_) {
|
||||
cb.invoker(cb.func_ptr, cb.context, args...);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr size_t size() const { return callbacks_.size(); }
|
||||
};
|
||||
```
|
||||
|
||||
**C++20 Benefits:**
|
||||
- ✅ **Concepts** provide clear compile errors
|
||||
- ✅ **Designated initializers** make code more readable
|
||||
- ✅ **consteval** ensures compile-time evaluation
|
||||
- ✅ **constexpr** improvements allow more compile-time validation
|
||||
- ✅ **Requires clauses** document constraints inline
|
||||
|
||||
**Usage Changes:**
|
||||
|
||||
```cpp
|
||||
// OLD (lambda):
|
||||
parent_->add_scanner_state_callback([this](ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
});
|
||||
|
||||
// NEW (static function + context):
|
||||
static void scanner_state_callback(BluetoothProxy* proxy, ScannerState state) {
|
||||
if (proxy->api_connection_ != nullptr) {
|
||||
proxy->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
}
|
||||
|
||||
// Registration
|
||||
parent_->add_scanner_state_callback(scanner_state_callback, this);
|
||||
```
|
||||
|
||||
**Savings:**
|
||||
- **Per callback:** 24 bytes (32 → 8) or 20 bytes (32 → 12 for simpler version)
|
||||
- **RAM saved (100-150 callbacks):** 2.4 - 3.6 KB
|
||||
- **Flash saved:** ~5-10 KB (eliminates std::function template instantiations)
|
||||
|
||||
**Pros:**
|
||||
- ✅ Maximum memory savings (75% reduction)
|
||||
- ✅ Type-safe at registration time
|
||||
- ✅ No virtual function overhead
|
||||
- ✅ Works with all capture patterns
|
||||
- ✅ Simple implementation
|
||||
|
||||
**Cons:**
|
||||
- ❌ Requires converting lambdas to static functions
|
||||
- ❌ Changes API for all 67 CallbackManager users
|
||||
- ❌ More verbose at call site
|
||||
|
||||
---
|
||||
|
||||
### Option 2: Member Function Pointers
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*invoker)(void*, Ts...); // 4 bytes
|
||||
void* obj; // 4 bytes
|
||||
// Total: 8 bytes
|
||||
};
|
||||
|
||||
template<typename T, void (T::*Method)(Ts...)>
|
||||
static void invoke_member(void* obj, Ts... args) {
|
||||
(static_cast<T*>(obj)->*Method)(args...);
|
||||
}
|
||||
|
||||
std::vector<Callback> callbacks_;
|
||||
|
||||
public:
|
||||
// Register a member function
|
||||
template<typename T, void (T::*Method)(Ts...)>
|
||||
void add(T* obj) {
|
||||
callbacks_.push_back({
|
||||
&invoke_member<T, Method>,
|
||||
obj
|
||||
});
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto& cb : callbacks_) {
|
||||
cb.invoker(cb.obj, args...);
|
||||
}
|
||||
}
|
||||
|
||||
size_t size() const { return callbacks_.size(); }
|
||||
};
|
||||
```
|
||||
|
||||
**Usage Changes:**
|
||||
|
||||
```cpp
|
||||
// Add a method to BluetoothProxy
|
||||
void BluetoothProxy::on_scanner_state_changed(ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
}
|
||||
|
||||
// Register it
|
||||
parent_->add_scanner_state_callback<BluetoothProxy,
|
||||
&BluetoothProxy::on_scanner_state_changed>(this);
|
||||
```
|
||||
|
||||
**Savings:**
|
||||
- **Per callback:** 24 bytes (32 → 8)
|
||||
- **RAM saved:** 2.4 - 3.6 KB
|
||||
- **Flash saved:** ~5-10 KB
|
||||
|
||||
**Pros:**
|
||||
- ✅ Same memory savings as Option 1
|
||||
- ✅ Most type-safe (member function pointers)
|
||||
- ✅ No static functions needed
|
||||
- ✅ Clean separation of callback logic
|
||||
|
||||
**Cons:**
|
||||
- ❌ Verbose syntax at registration: `add<Type, &Type::method>(this)`
|
||||
- ❌ Requires adding methods to classes
|
||||
- ❌ Can't capture additional state beyond `this`
|
||||
- ❌ Template parameters at call site are ugly
|
||||
|
||||
---
|
||||
|
||||
### Option 3: Hybrid (Backward Compatible)
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*invoker)(void*, Ts...); // 4 bytes
|
||||
void* data; // 4 bytes
|
||||
bool is_std_function; // 1 byte + 3 padding = 4 bytes
|
||||
// Total: 12 bytes
|
||||
};
|
||||
|
||||
std::vector<Callback> callbacks_;
|
||||
|
||||
public:
|
||||
// Optimized: function pointer + context
|
||||
template<typename Context>
|
||||
void add(void (*func)(Context*, Ts...), Context* context) {
|
||||
callbacks_.push_back({
|
||||
[](void* ctx, Ts... args) {
|
||||
auto cb = static_cast<Callback*>(ctx);
|
||||
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(cb->data);
|
||||
auto typed_ctx = static_cast<Context*>(*reinterpret_cast<void**>(
|
||||
static_cast<char*>(cb) + offsetof(Callback, data)
|
||||
));
|
||||
typed_func(typed_ctx, args...);
|
||||
},
|
||||
reinterpret_cast<void*>(func),
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy: std::function support (for gradual migration)
|
||||
void add(std::function<void(Ts...)>&& func) {
|
||||
auto* stored = new std::function<void(Ts...)>(std::move(func));
|
||||
callbacks_.push_back({
|
||||
[](void* ctx, Ts... args) {
|
||||
(*static_cast<std::function<void(Ts...)>*>(ctx))(args...);
|
||||
},
|
||||
stored,
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
~CallbackManager() {
|
||||
for (auto& cb : callbacks_) {
|
||||
if (cb.is_std_function) {
|
||||
delete static_cast<std::function<void(Ts...)>*>(cb.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto& cb : callbacks_) {
|
||||
cb.invoker(&cb, args...);
|
||||
}
|
||||
}
|
||||
|
||||
size_t size() const { return callbacks_.size(); }
|
||||
};
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```cpp
|
||||
// NEW (optimized):
|
||||
parent_->add_scanner_state_callback(scanner_state_callback, this);
|
||||
|
||||
// OLD (still works - gradual migration):
|
||||
parent_->add_scanner_state_callback([this](ScannerState state) {
|
||||
// ... lambda still works
|
||||
});
|
||||
```
|
||||
|
||||
**Savings:**
|
||||
- **Per optimized callback:** 20 bytes (32 → 12)
|
||||
- **Per legacy callback:** 0 bytes (still uses std::function)
|
||||
- **Allows gradual migration**
|
||||
|
||||
**Pros:**
|
||||
- ✅ Backward compatible
|
||||
- ✅ Gradual migration path
|
||||
- ✅ Mix optimized and legacy in same codebase
|
||||
- ✅ No breaking changes
|
||||
|
||||
**Cons:**
|
||||
- ❌ More complex implementation
|
||||
- ❌ Need to track which callbacks need cleanup
|
||||
- ❌ Extra bool field (padding makes it 12 bytes instead of 8)
|
||||
- ❌ std::function still compiled in
|
||||
|
||||
---
|
||||
|
||||
### Option 4: FixedVector (Keep std::function, Optimize Vector)
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
public:
|
||||
void add(std::function<void(Ts...)> &&callback) {
|
||||
if (this->callbacks_.empty()) {
|
||||
// Most CallbackManagers have 1-5 callbacks
|
||||
this->callbacks_.init(5);
|
||||
}
|
||||
this->callbacks_.push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto &cb : this->callbacks_)
|
||||
cb(args...);
|
||||
}
|
||||
|
||||
size_t size() const { return this->callbacks_.size(); }
|
||||
|
||||
protected:
|
||||
FixedVector<std::function<void(Ts...)>> callbacks_; // Changed from std::vector
|
||||
};
|
||||
```
|
||||
|
||||
**Savings:**
|
||||
- **Per callback:** 0 bytes (still 32 bytes)
|
||||
- **Per instance:** ~132 bytes (eliminates `_M_realloc_append`)
|
||||
- **Flash saved:** ~5-10 KB (one less vector template instantiation per type)
|
||||
- **Total:** ~132 bytes × ~20 unique callback types = ~2.6 KB
|
||||
|
||||
**Pros:**
|
||||
- ✅ No API changes
|
||||
- ✅ Drop-in replacement
|
||||
- ✅ Eliminates vector reallocation machinery
|
||||
- ✅ Zero migration cost
|
||||
|
||||
**Cons:**
|
||||
- ❌ No per-callback savings
|
||||
- ❌ std::function still 32 bytes each
|
||||
- ❌ Must guess max size at runtime
|
||||
- ❌ Can still overflow if guess is wrong
|
||||
|
||||
---
|
||||
|
||||
### Option 5: Template Parameter for Storage (Advanced)
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```cpp
|
||||
enum class CallbackStorage {
|
||||
FUNCTION, // Use std::function (default, most flexible)
|
||||
FUNCTION_PTR // Use function pointer + context (optimal)
|
||||
};
|
||||
|
||||
template<typename... Ts, CallbackStorage Storage = CallbackStorage::FUNCTION>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
// Specialize implementation based on Storage parameter
|
||||
};
|
||||
|
||||
// Default: std::function (backward compatible)
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...), CallbackStorage::FUNCTION> {
|
||||
protected:
|
||||
std::vector<std::function<void(Ts...)>> callbacks_;
|
||||
// ... current implementation
|
||||
};
|
||||
|
||||
// Optimized: function pointer + context
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...), CallbackStorage::FUNCTION_PTR> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*func)(void*, Ts...);
|
||||
void* context;
|
||||
};
|
||||
std::vector<Callback> callbacks_;
|
||||
// ... Option 1 implementation
|
||||
};
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```cpp
|
||||
// Old components (no changes):
|
||||
CallbackManager<void(float)> callback_; // Uses std::function by default
|
||||
|
||||
// Optimized components:
|
||||
CallbackManager<void(ScannerState), CallbackStorage::FUNCTION_PTR> scanner_state_callbacks_;
|
||||
```
|
||||
|
||||
**Savings:**
|
||||
- **Opt-in per component**
|
||||
- **Same as Option 1 for optimized components**
|
||||
|
||||
**Pros:**
|
||||
- ✅ Gradual migration
|
||||
- ✅ No breaking changes
|
||||
- ✅ Explicit opt-in per component
|
||||
- ✅ Clear which components are optimized
|
||||
|
||||
**Cons:**
|
||||
- ❌ Complex template metaprogramming
|
||||
- ❌ Two implementations to maintain
|
||||
- ❌ Template parameter pollution
|
||||
- ❌ Harder to understand codebase
|
||||
|
||||
---
|
||||
|
||||
## Comparison Matrix
|
||||
|
||||
| Option | Per-Callback Savings | Flash Savings | API Changes | Complexity | Migration Cost |
|
||||
|--------|---------------------|---------------|-------------|------------|----------------|
|
||||
| **1. Function Ptr + Context** | **24 bytes** (75%) | **~10 KB** | Yes | Low | High (67 files) |
|
||||
| **2. Member Function Ptrs** | **24 bytes** (75%) | **~10 KB** | Yes | Medium | High + class changes |
|
||||
| **3. Hybrid** | **20 bytes** (opt-in) | **~8 KB** | No | High | Low (gradual) |
|
||||
| **4. FixedVector** | **0 bytes** | **~3 KB** | No | Low | None |
|
||||
| **5. Template Parameter** | **24 bytes** (opt-in) | **~10 KB** | Optional | High | Medium |
|
||||
|
||||
---
|
||||
|
||||
## Migration Effort Estimate
|
||||
|
||||
### Option 1 (Function Pointer + Context)
|
||||
|
||||
**Files to change:** ~67 files with CallbackManager usage
|
||||
|
||||
**Per-file changes:**
|
||||
1. Convert lambda to static function (5 min)
|
||||
2. Update registration call (1 min)
|
||||
3. Test (5 min)
|
||||
|
||||
**Estimate:** ~11 min × 67 files = **~12 hours** (assuming some files have multiple callbacks)
|
||||
|
||||
**High-impact components to prioritize:**
|
||||
- `sensor.h` / `sensor.cpp` - many sensor callbacks
|
||||
- `esp32_ble_tracker.h` - BLE callbacks
|
||||
- `climate.h` - climate callbacks
|
||||
- `binary_sensor.h` - binary sensor callbacks
|
||||
|
||||
### Option 4 (FixedVector)
|
||||
|
||||
**Files to change:** 1 file (`esphome/core/helpers.h`)
|
||||
|
||||
**Changes:**
|
||||
1. Change `std::vector` to `FixedVector` in CallbackManager
|
||||
2. Initialize with reasonable default size (e.g., 5)
|
||||
3. Test across codebase
|
||||
|
||||
**Estimate:** **~1 hour**
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate Action: Option 4 (FixedVector)
|
||||
|
||||
**Why:**
|
||||
- Zero migration cost
|
||||
- Immediate ~3 KB flash savings
|
||||
- No API changes
|
||||
- Low risk
|
||||
|
||||
**Implementation:**
|
||||
```cpp
|
||||
template<typename... Ts> class CallbackManager<void(Ts...)> {
|
||||
public:
|
||||
void add(std::function<void(Ts...)> &&callback) {
|
||||
if (this->callbacks_.empty()) {
|
||||
this->callbacks_.init(8); // Most have < 8 callbacks
|
||||
}
|
||||
this->callbacks_.push_back(std::move(callback));
|
||||
}
|
||||
// ... rest unchanged
|
||||
protected:
|
||||
FixedVector<std::function<void(Ts...)>> callbacks_;
|
||||
};
|
||||
```
|
||||
|
||||
### Long-term: Option 1 (Function Pointer + Context)
|
||||
|
||||
**Why:**
|
||||
- Maximum savings (2.4-3.6 KB RAM + 10 KB flash)
|
||||
- Clean, simple implementation
|
||||
- Type-safe
|
||||
- Well-tested pattern
|
||||
|
||||
**Migration Strategy:**
|
||||
1. Implement new `CallbackManager` in `helpers.h`
|
||||
2. Migrate high-impact components first:
|
||||
- Core components (sensor, binary_sensor, climate)
|
||||
- BLE components (esp32_ble_tracker, bluetooth_proxy)
|
||||
- Network components (api, mqtt)
|
||||
3. Create helper macros to reduce boilerplate
|
||||
4. Migrate remaining components over 2-3 releases
|
||||
|
||||
**Helper Macro Example:**
|
||||
```cpp
|
||||
// Define a callback wrapper
|
||||
#define CALLBACK_WRAPPER(Class, Method, ...) \
|
||||
static void Method##_callback(Class* self, ##__VA_ARGS__) { \
|
||||
self->Method(__VA_ARGS__); \
|
||||
}
|
||||
|
||||
// In class:
|
||||
class BluetoothProxy {
|
||||
CALLBACK_WRAPPER(BluetoothProxy, on_scanner_state, ScannerState state)
|
||||
|
||||
void on_scanner_state(ScannerState state) {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
void setup() {
|
||||
parent_->add_scanner_state_callback(on_scanner_state_callback, this);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Phase 1: Unit Tests
|
||||
- Test CallbackManager with various signatures
|
||||
- Test multiple callbacks (1, 5, 10, 50)
|
||||
- Test callback removal/cancellation
|
||||
- Test edge cases (empty, nullptr, etc.)
|
||||
|
||||
### Phase 2: Integration Tests
|
||||
- Create test YAML with heavily-used callbacks
|
||||
- Run on ESP32, ESP8266, RP2040
|
||||
- Measure before/after memory usage
|
||||
- Verify no functional regressions
|
||||
|
||||
### Phase 3: Component Tests
|
||||
- Test high-impact components:
|
||||
- sensor with multiple state callbacks
|
||||
- esp32_improv with all automation triggers
|
||||
- climate with state/control callbacks
|
||||
- Measure memory with `esphome analyze-memory`
|
||||
|
||||
---
|
||||
|
||||
## Risk Analysis
|
||||
|
||||
### Option 1 Risks
|
||||
|
||||
**Risk: Breaking change across 67 files**
|
||||
- **Mitigation:** Gradual rollout over 2-3 releases
|
||||
- **Mitigation:** Extensive testing on real hardware
|
||||
|
||||
**Risk: Static function verbosity**
|
||||
- **Mitigation:** Helper macros (see above)
|
||||
- **Mitigation:** Code generation from Python
|
||||
|
||||
**Risk: Missing captures**
|
||||
- **Mitigation:** Static analysis to find lambda captures
|
||||
- **Mitigation:** Compile-time errors for incorrect usage
|
||||
|
||||
### Option 4 Risks
|
||||
|
||||
**Risk: Buffer overflow if size guess is wrong**
|
||||
- **Mitigation:** Choose conservative default (8)
|
||||
- **Mitigation:** Add runtime warning on resize
|
||||
- **Mitigation:** Monitor in CI/testing
|
||||
|
||||
**Risk: Still uses std::function (32 bytes each)**
|
||||
- **Mitigation:** Follow up with Option 1 migration
|
||||
- **Mitigation:** This is a stepping stone, not final solution
|
||||
|
||||
---
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
### Week 1: Option 4 (Quick Win)
|
||||
- Implement FixedVector in CallbackManager
|
||||
- Test across codebase
|
||||
- Create PR with memory analysis
|
||||
- **Expected savings:** ~3 KB flash
|
||||
|
||||
### Month 1-2: Option 1 (Core Components)
|
||||
- Implement function pointer CallbackManager
|
||||
- Migrate sensor, binary_sensor, climate
|
||||
- Create helper macros
|
||||
- **Expected savings:** ~1 KB RAM + 5 KB flash
|
||||
|
||||
### Month 3-4: Option 1 (Remaining Components)
|
||||
- Migrate BLE components
|
||||
- Migrate network components (api, mqtt)
|
||||
- Migrate automation components
|
||||
- **Expected savings:** ~2 KB RAM + 10 KB flash total
|
||||
|
||||
### Month 5: Cleanup
|
||||
- Remove std::function CallbackManager
|
||||
- Update documentation
|
||||
- Blog post about optimization
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Recommended Approach:**
|
||||
|
||||
1. **Immediate (Week 1):** Implement Option 4 (FixedVector)
|
||||
- Low risk, zero migration cost
|
||||
- ~3 KB flash savings
|
||||
- Sets foundation for Option 1
|
||||
|
||||
2. **Short-term (Month 1-2):** Begin Option 1 migration
|
||||
- Start with high-impact components
|
||||
- ~1-2 KB RAM + 5 KB flash savings
|
||||
- Validate approach
|
||||
|
||||
3. **Long-term (Month 3-6):** Complete Option 1 migration
|
||||
- Migrate all components
|
||||
- ~3-4 KB total RAM + 10 KB flash savings
|
||||
- Remove std::function variant
|
||||
|
||||
**Total Expected Savings:**
|
||||
- **RAM:** 2.4 - 3.6 KB (75% reduction per callback)
|
||||
- **Flash:** 8 - 13 KB (vector overhead + template instantiations)
|
||||
- **Performance:** Slightly faster (no std::function indirection)
|
||||
|
||||
This is significant for ESP8266 (80 KB RAM, 1 MB flash) and beneficial for all platforms.
|
||||
75
callback_optimization_analysis.md
Normal file
75
callback_optimization_analysis.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Callback Optimization Analysis - Why It Failed
|
||||
|
||||
## Goal
|
||||
Convert stateful lambdas in CallbackManager to stateless function pointers to reduce flash usage.
|
||||
|
||||
## Approach Tested
|
||||
|
||||
### Attempt 1: Discriminated Union in CallbackManager
|
||||
**Changed:** `CallbackManager` to use union with discriminator (like `TemplatableValue`)
|
||||
- Stateless lambdas → function pointer (8 bytes)
|
||||
- Stateful lambdas → heap-allocated `std::function*` (8 bytes struct + 32 bytes heap)
|
||||
|
||||
**Result:**
|
||||
- ❌ **+300 bytes heap usage** (37-38 callbacks × 8 bytes overhead)
|
||||
- ✅ Flash savings potential: ~200-400 bytes per stateless callback
|
||||
- **Verdict:** RAM is more precious than flash on ESP8266 - rejected
|
||||
|
||||
### Attempt 2: Convert Individual Callbacks to Stateless
|
||||
**Changed:** API logger callback from `[this]` lambda to static member function
|
||||
- Used existing `global_api_server` pointer
|
||||
- Made callback stateless (convertible to function pointer)
|
||||
|
||||
**Result:**
|
||||
```
|
||||
Removed:
|
||||
- Lambda _M_invoke: 103 bytes
|
||||
- Lambda _M_manager: 20 bytes
|
||||
|
||||
Added:
|
||||
- log_callback function: 104 bytes
|
||||
- Function pointer _M_invoke: 20 bytes
|
||||
- Function pointer _M_manager: 20 bytes
|
||||
- Larger setup(): 7 bytes
|
||||
|
||||
Net: +32 bytes flash ❌
|
||||
```
|
||||
|
||||
**Why it failed:**
|
||||
Even though the callback became stateless, `CallbackManager` still uses `std::vector<std::function<void(Ts...)>>`. The function pointer STILL gets wrapped in `std::function`, generating the same template instantiation overhead. We just moved the code from a lambda to a static function.
|
||||
|
||||
## Root Cause
|
||||
|
||||
The optimization **requires BOTH**:
|
||||
1. ✅ Stateless callback (function pointer)
|
||||
2. ❌ Modified `CallbackManager` to store function pointers directly without `std::function` wrapper
|
||||
|
||||
Without modifying `CallbackManager`, converting individual callbacks to function pointers provides **no benefit** and actually **increases** code size slightly due to the extra function definition.
|
||||
|
||||
## Conclusion
|
||||
|
||||
This optimization path is a **dead end** for ESPHome because:
|
||||
1. **Discriminated union approach**: Increases heap by 300 bytes (unacceptable for ESP8266)
|
||||
2. **Individual callback conversion**: Increases flash by 32+ bytes (no benefit without CallbackManager changes)
|
||||
|
||||
The current `std::vector<std::function<...>>` approach is already optimal for the use case where most callbacks capture state.
|
||||
|
||||
## Alternative Approaches Considered
|
||||
|
||||
1. **Create separate `StatelessCallbackManager`**: Would require changing all call sites, not worth the complexity
|
||||
2. **Template parameter to select storage type**: Same issue - requires modifying many components
|
||||
3. **Hand-pick specific callbacks**: Provides no benefit as shown in Attempt 2
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Do not pursue this optimization.** The RAM/flash trade-offs are unfavorable for embedded systems where RAM is typically more constrained than flash.
|
||||
|
||||
---
|
||||
|
||||
**Test Results:**
|
||||
- Platform: ESP8266-Arduino
|
||||
- Component: API
|
||||
- Result: +32 bytes flash (0.01% increase)
|
||||
- Status: Reverted
|
||||
|
||||
🤖 Analysis by Claude Code
|
||||
256
callback_optimization_implementation_plan.md
Normal file
256
callback_optimization_implementation_plan.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# Callback Optimization Implementation Plan
|
||||
|
||||
## Analysis Summary
|
||||
|
||||
After Controller Registry (PR #11772), callback infrastructure can be further optimized:
|
||||
|
||||
**Current overhead per entity (ESP32 32-bit):**
|
||||
- No callbacks: 16 bytes (4-byte ptr + 12-byte empty vector)
|
||||
- With callbacks: 32+ bytes (16 baseline + 16+ per callback)
|
||||
|
||||
**Opportunity:** After Controller Registry, most entities have **zero callbacks** (API/WebServer use registry instead). We can save 12 bytes per entity by lazy allocation.
|
||||
|
||||
## Entity Types by Callback Needs
|
||||
|
||||
### Entities with ONLY filtered callbacks (most)
|
||||
- Climate, Fan, Light, Cover
|
||||
- Switch, Lock, Valve
|
||||
- Number, Select, Text, Button
|
||||
- AlarmControlPanel, MediaPlayer
|
||||
- BinarySensor, Event, Update, DateTime
|
||||
|
||||
**Optimization:** Simple lazy-allocated vector
|
||||
|
||||
### Entities with raw AND filtered callbacks
|
||||
- **Sensor** - has raw callbacks for automation triggers
|
||||
- **TextSensor** - has raw callbacks for automation triggers
|
||||
|
||||
**Optimization:** Partitioned vector (filtered | raw)
|
||||
|
||||
## Proposed Implementations
|
||||
|
||||
### Option 1: Simple Lazy Vector (for entities without raw callbacks)
|
||||
|
||||
```cpp
|
||||
class Climate {
|
||||
protected:
|
||||
std::unique_ptr<std::vector<std::function<void(Climate&)>>> state_callback_;
|
||||
};
|
||||
|
||||
void Climate::add_on_state_callback(std::function<void(Climate&)> &&callback) {
|
||||
if (!this->state_callback_) {
|
||||
this->state_callback_ = std::make_unique<std::vector<std::function<void(Climate&)>>>();
|
||||
}
|
||||
this->state_callback_->push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void Climate::publish_state() {
|
||||
if (this->state_callback_) {
|
||||
for (auto &cb : *this->state_callback_) {
|
||||
cb(*this);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Memory (ESP32):**
|
||||
- No callbacks: 4 bytes (saves 12 vs current)
|
||||
- 1 callback: 36 bytes (costs 4 vs current)
|
||||
- Net: Positive for API-only devices
|
||||
|
||||
### Option 2: Partitioned Vector (for Sensor & TextSensor)
|
||||
|
||||
```cpp
|
||||
class Sensor {
|
||||
protected:
|
||||
struct Callbacks {
|
||||
std::vector<std::function<void(float)>> callbacks_;
|
||||
uint8_t filtered_count_{0}; // Partition point: [filtered | raw]
|
||||
|
||||
void add_filtered(std::function<void(float)> &&fn) {
|
||||
callbacks_.push_back(std::move(fn));
|
||||
if (filtered_count_ < callbacks_.size() - 1) {
|
||||
std::swap(callbacks_[filtered_count_], callbacks_[callbacks_.size() - 1]);
|
||||
}
|
||||
filtered_count_++;
|
||||
}
|
||||
|
||||
void add_raw(std::function<void(float)> &&fn) {
|
||||
callbacks_.push_back(std::move(fn)); // Append to raw section
|
||||
}
|
||||
|
||||
void call_filtered(float value) {
|
||||
for (size_t i = 0; i < filtered_count_; i++) {
|
||||
callbacks_[i](value);
|
||||
}
|
||||
}
|
||||
|
||||
void call_raw(float value) {
|
||||
for (size_t i = filtered_count_; i < callbacks_.size(); i++) {
|
||||
callbacks_[i](value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Callbacks> callbacks_;
|
||||
};
|
||||
```
|
||||
|
||||
**Why partitioned:**
|
||||
- Maintains separation of raw (pre-filter) vs filtered (post-filter) callbacks
|
||||
- O(1) insertion via swap (order doesn't matter)
|
||||
- No branching in hot path
|
||||
- Saves 12 bytes when no callbacks
|
||||
|
||||
## Memory Impact Analysis
|
||||
|
||||
### Scenario 1: API-only device (10 sensors, no MQTT, no automations)
|
||||
**Current:** 10 × 16 = 160 bytes
|
||||
**Optimized:** 10 × 4 = 40 bytes
|
||||
**Saves: 120 bytes** ✅
|
||||
|
||||
### Scenario 2: MQTT-enabled device (10 sensors with MQTT)
|
||||
**Current:** 10 × 32 = 320 bytes
|
||||
**Optimized:** 10 × 36 = 360 bytes
|
||||
**Costs: 40 bytes** ⚠️
|
||||
|
||||
### Scenario 3: Mixed device (5 API-only + 5 MQTT)
|
||||
**Current:** (5 × 16) + (5 × 32) = 240 bytes
|
||||
**Optimized:** (5 × 4) + (5 × 36) = 200 bytes
|
||||
**Saves: 40 bytes** ✅
|
||||
|
||||
### Scenario 4: Sensor with automation (1 raw + 1 filtered)
|
||||
**Current:** 16 + 12 + 16 + 16 = 60 bytes
|
||||
**Optimized:** 4 + 16 + 32 = 52 bytes
|
||||
**Saves: 8 bytes** ✅
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Phase 1: Simple Entities (high impact, low complexity)
|
||||
1. **Climate** (common, no raw callbacks)
|
||||
2. **Fan** (common, no raw callbacks)
|
||||
3. **Cover** (common, no raw callbacks)
|
||||
4. **Switch** (very common, no raw callbacks)
|
||||
5. **Lock** (no raw callbacks)
|
||||
|
||||
**Change:** Replace `CallbackManager<void(...)> callback_` with `std::unique_ptr<std::vector<std::function<...>>>`
|
||||
|
||||
### Phase 2: Sensor & TextSensor (more complex)
|
||||
1. **Sensor** (most common entity, has raw callbacks)
|
||||
2. **TextSensor** (common, has raw callbacks)
|
||||
|
||||
**Change:** Implement partitioned vector approach
|
||||
|
||||
### Phase 3: Remaining Entities
|
||||
- BinarySensor, Number, Select, Text
|
||||
- Light, Valve, AlarmControlPanel
|
||||
- MediaPlayer, Button, Event, Update, DateTime
|
||||
|
||||
**Change:** Simple lazy vector
|
||||
|
||||
## Code Template for Simple Entities
|
||||
|
||||
```cpp
|
||||
// Header (.h)
|
||||
class EntityType {
|
||||
public:
|
||||
void add_on_state_callback(std::function<void(Args...)> &&callback);
|
||||
|
||||
protected:
|
||||
std::unique_ptr<std::vector<std::function<void(Args...)>>> state_callback_;
|
||||
};
|
||||
|
||||
// Implementation (.cpp)
|
||||
void EntityType::add_on_state_callback(std::function<void(Args...)> &&callback) {
|
||||
if (!this->state_callback_) {
|
||||
this->state_callback_ = std::make_unique<std::vector<std::function<void(Args...)>>>();
|
||||
}
|
||||
this->state_callback_->push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void EntityType::publish_state(...) {
|
||||
// ... state update logic ...
|
||||
|
||||
if (this->state_callback_) {
|
||||
for (auto &cb : *this->state_callback_) {
|
||||
cb(...);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_CONTROLLER_REGISTRY
|
||||
ControllerRegistry::notify_entity_update(this);
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Unit tests:** Verify callback ordering/execution unchanged
|
||||
2. **Integration tests:** Test with MQTT, automations, copy components
|
||||
3. **Memory benchmarks:** Measure actual flash/RAM impact
|
||||
4. **Compatibility:** Ensure no API breakage
|
||||
|
||||
## Expected Results
|
||||
|
||||
**For typical ESPHome devices after Controller Registry:**
|
||||
- Most entities: API/WebServer only (no callbacks)
|
||||
- Some entities: MQTT (1 callback)
|
||||
- Few entities: Automations (1-2 callbacks)
|
||||
|
||||
**Memory savings:**
|
||||
- Device with 20 entities, 5 with MQTT: ~180 bytes saved
|
||||
- Device with 50 entities, 10 with MQTT: ~480 bytes saved
|
||||
|
||||
**Trade-off:**
|
||||
- Entities without callbacks: Save 12 bytes ✅
|
||||
- Entities with callbacks: Cost 4 bytes ⚠️
|
||||
- Net benefit: Positive for most devices
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
**Risk 1:** Increased complexity
|
||||
- **Mitigation:** Start with simple entities first, template for reuse
|
||||
|
||||
**Risk 2:** Performance regression
|
||||
- **Mitigation:** Minimal - just nullptr check (likely free with branch prediction)
|
||||
|
||||
**Risk 3:** Edge cases with callback order
|
||||
- **Mitigation:** Order already undefined within same callback type
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. Should we template the Callbacks struct for reuse across entity types?
|
||||
2. Should Phase 1 include a memory benchmark before expanding?
|
||||
3. Should we make this configurable (compile-time flag)?
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Phase 1 (Simple Entities)
|
||||
- `esphome/components/climate/climate.h`
|
||||
- `esphome/components/climate/climate.cpp`
|
||||
- `esphome/components/fan/fan.h`
|
||||
- `esphome/components/fan/fan.cpp`
|
||||
- `esphome/components/cover/cover.h`
|
||||
- `esphome/components/cover/cover.cpp`
|
||||
- (etc. for switch, lock)
|
||||
|
||||
### Phase 2 (Partitioned)
|
||||
- `esphome/components/sensor/sensor.h`
|
||||
- `esphome/components/sensor/sensor.cpp`
|
||||
- `esphome/components/text_sensor/text_sensor.h`
|
||||
- `esphome/components/text_sensor/text_sensor.cpp`
|
||||
|
||||
### Phase 3 (Remaining)
|
||||
- All other entity types
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Recommendation: Implement in phases**
|
||||
|
||||
1. Start with Climate (common entity, simple change)
|
||||
2. Measure impact on real device
|
||||
3. If positive, proceed with other simple entities
|
||||
4. Implement partitioned approach for Sensor/TextSensor
|
||||
5. Complete remaining entity types
|
||||
|
||||
Expected net savings: **50-500 bytes per typical device**, depending on entity count and MQTT usage.
|
||||
118
callback_usage_analysis.md
Normal file
118
callback_usage_analysis.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# add_on_state_callback Usage Analysis
|
||||
|
||||
## Summary
|
||||
|
||||
After the Controller Registry migration (PR #11772), `add_on_state_callback` is still widely used in the codebase, but for **legitimate reasons** - components that genuinely need per-entity state tracking.
|
||||
|
||||
## Usage Breakdown
|
||||
|
||||
### 1. **MQTT Components** (~17 uses)
|
||||
**Purpose:** Per-entity MQTT configuration requires callbacks
|
||||
- Each MQTT component instance needs to publish to custom topics with custom QoS/retain settings
|
||||
- Cannot use Controller pattern due to per-entity configuration overhead
|
||||
- Examples: `mqtt_sensor.cpp`, `mqtt_climate.cpp`, `mqtt_number.cpp`, etc.
|
||||
|
||||
```cpp
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
this->publish_state(state);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. **Copy Components** (~10 uses)
|
||||
**Purpose:** Mirror state from one entity to another
|
||||
- Each copy instance tracks a different source entity
|
||||
- Legitimate use of callbacks for entity-to-entity synchronization
|
||||
- Examples: `copy_sensor.cpp`, `copy_fan.cpp`, `copy_select.cpp`, etc.
|
||||
|
||||
```cpp
|
||||
source_->add_on_state_callback([this](const std::string &value) {
|
||||
this->publish_state(value);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. **Derivative Sensors** (~5-7 uses)
|
||||
**Purpose:** Compute derived values from source sensors
|
||||
- **integration_sensor:** Integrates sensor values over time
|
||||
- **total_daily_energy:** Tracks cumulative energy
|
||||
- **combination:** Combines multiple sensor values
|
||||
- **graph:** Samples sensor data for display
|
||||
- **duty_time:** Tracks on-time duration
|
||||
- **ntc/absolute_humidity/resistance:** Mathematical transformations
|
||||
|
||||
```cpp
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
this->process_sensor_value_(state);
|
||||
});
|
||||
```
|
||||
|
||||
### 4. **Climate/Cover with Sensors** (~10-15 uses)
|
||||
**Purpose:** External sensors providing feedback to control loops
|
||||
- **feedback_cover:** Binary sensors for open/close/obstacle detection
|
||||
- **bang_bang/pid/thermostat:** External temperature sensors for climate control
|
||||
- **climate_ir (toshiba/yashima/heatpumpir):** Temperature sensors for IR climate
|
||||
|
||||
```cpp
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
this->current_temperature = state;
|
||||
// Trigger control loop update
|
||||
});
|
||||
```
|
||||
|
||||
### 5. **Entity Base Classes** (~10-15 definitions)
|
||||
**Purpose:** Provide the callback interface for all entities
|
||||
- Not actual usage, just the method definitions
|
||||
- Examples: `sensor.cpp::add_on_state_callback()`, `climate.cpp::add_on_state_callback()`, etc.
|
||||
|
||||
### 6. **Automation Trigger Classes** (~15-20 definitions)
|
||||
**Purpose:** User-defined YAML automations need callbacks
|
||||
- Files like `sensor/automation.h`, `climate/automation.h`
|
||||
- Implement triggers like `on_value:`, `on_state:`
|
||||
- Cannot be migrated - this is user-facing automation functionality
|
||||
|
||||
### 7. **Miscellaneous** (~5-10 uses)
|
||||
- **voice_assistant/micro_wake_word:** State coordination
|
||||
- **esp32_improv:** Provisioning state tracking
|
||||
- **http_request/update:** Update status monitoring
|
||||
- **switch/binary_sensor:** Cross-component dependencies
|
||||
- **OTA callbacks:** OTA state monitoring
|
||||
|
||||
## Key Insights
|
||||
|
||||
### What's NOT Using Callbacks Anymore ✅
|
||||
**API Server and WebServer** - migrated to Controller Registry
|
||||
- **Before:** Each entity had 2 callbacks (API + WebServer) = ~32 bytes overhead
|
||||
- **After:** Zero per-entity overhead = saves ~32 bytes per entity
|
||||
|
||||
### What SHOULD Keep Using Callbacks ✅
|
||||
All the above categories have legitimate reasons:
|
||||
|
||||
1. **Per-entity configuration:** MQTT needs custom topics/QoS per entity
|
||||
2. **Entity-to-entity relationships:** Copy components, derivative sensors
|
||||
3. **Control loop feedback:** Climate/cover with external sensors
|
||||
4. **User-defined automations:** YAML triggers configured by users
|
||||
5. **Component dependencies:** Components that genuinely depend on other entities
|
||||
|
||||
## Memory Impact
|
||||
|
||||
**Per Sensor (ESP32):**
|
||||
- Empty callback infrastructure: **~16 bytes** (unique_ptr + empty vector)
|
||||
- With one callback (e.g., MQTT): **~32 bytes** (16 + std::function)
|
||||
- With multiple callbacks: **~32 + 16n bytes** (where n = additional callbacks)
|
||||
|
||||
**Typical scenarios:**
|
||||
- Sensor with **only API/WebServer:** ~16 bytes (no callbacks registered)
|
||||
- Sensor with **MQTT:** ~32 bytes (one callback)
|
||||
- Sensor with **MQTT + automation:** ~48 bytes (two callbacks)
|
||||
- Sensor with **copy + total_daily_energy + graph:** ~64 bytes (three callbacks)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The callback system is still heavily used (~103 occurrences) but for **appropriate reasons**:
|
||||
- Components with per-entity state/configuration (MQTT, Copy)
|
||||
- Sensor processing chains (derivatives, transformations)
|
||||
- Control loops with external feedback (climate, covers)
|
||||
- User-defined automations (cannot be removed)
|
||||
|
||||
The Controller Registry successfully eliminated wasteful callbacks for **stateless global handlers** (API/WebServer), saving ~32 bytes per entity for those use cases.
|
||||
|
||||
**No further callback elimination opportunities** exist without fundamentally changing ESPHome's architecture or breaking user-facing features.
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <utility>
|
||||
|
||||
#include "alarm_control_panel.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -34,6 +36,9 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
|
||||
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
|
||||
this->current_state_ = state;
|
||||
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) {
|
||||
this->triggered_callback_.call();
|
||||
} else if (state == ACP_STATE_ARMING) {
|
||||
|
||||
@@ -244,6 +244,9 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
# Track controller registration for StaticVector sizing
|
||||
CORE.register_controller()
|
||||
|
||||
cg.add(var.set_port(config[CONF_PORT]))
|
||||
if config[CONF_PASSWORD]:
|
||||
cg.add_define("USE_API_PASSWORD")
|
||||
|
||||
@@ -2147,7 +2147,7 @@ message ListEntitiesEventResponse {
|
||||
EntityCategory entity_category = 7;
|
||||
string device_class = 8;
|
||||
|
||||
repeated string event_types = 9;
|
||||
repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
message EventResponse {
|
||||
|
||||
@@ -193,11 +193,6 @@ void APIConnection::loop() {
|
||||
if (!this->deferred_batch_.empty()) {
|
||||
this->process_batch_();
|
||||
}
|
||||
// Release excess capacity from initial entity flood
|
||||
// deferred_batch_ grew up to MAX_INITIAL_PER_BATCH (24) items, now shrink to free up to ~384 bytes
|
||||
this->deferred_batch_.items.shrink_to_fit();
|
||||
// shared_write_buffer_ grew up to ~1.5KB during initial state, now shrink to free up to ~1.4KB
|
||||
this->parent_->get_shared_buffer_ref().shrink_to_fit();
|
||||
// Now that everything is sent, enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
}
|
||||
@@ -1315,8 +1310,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
|
||||
auto *event = static_cast<event::Event *>(entity);
|
||||
ListEntitiesEventResponse msg;
|
||||
msg.set_device_class(event->get_device_class_ref());
|
||||
for (const auto &event_type : event->get_event_types())
|
||||
msg.event_types.push_back(event_type);
|
||||
msg.event_types = &event->get_event_types();
|
||||
return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
|
||||
@@ -2877,8 +2877,8 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_bool(6, this->disabled_by_default);
|
||||
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
|
||||
buffer.encode_string(8, this->device_class_ref_);
|
||||
for (auto &it : this->event_types) {
|
||||
buffer.encode_string(9, it, true);
|
||||
for (const char *it : *this->event_types) {
|
||||
buffer.encode_string(9, it, strlen(it), true);
|
||||
}
|
||||
#ifdef USE_DEVICES
|
||||
buffer.encode_uint32(10, this->device_id);
|
||||
@@ -2894,9 +2894,9 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_bool(1, this->disabled_by_default);
|
||||
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
|
||||
size.add_length(1, this->device_class_ref_.size());
|
||||
if (!this->event_types.empty()) {
|
||||
for (const auto &it : this->event_types) {
|
||||
size.add_length_force(1, it.size());
|
||||
if (!this->event_types->empty()) {
|
||||
for (const char *it : *this->event_types) {
|
||||
size.add_length_force(1, strlen(it));
|
||||
}
|
||||
}
|
||||
#ifdef USE_DEVICES
|
||||
|
||||
@@ -2788,7 +2788,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage {
|
||||
#endif
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
std::vector<std::string> event_types{};
|
||||
const FixedVector<const char *> *event_types{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
|
||||
@@ -2053,7 +2053,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const {
|
||||
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, "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);
|
||||
}
|
||||
#ifdef USE_DEVICES
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
@@ -34,7 +35,7 @@ APIServer::APIServer() {
|
||||
}
|
||||
|
||||
void APIServer::setup() {
|
||||
this->setup_controller();
|
||||
ControllerRegistry::register_controller(this);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
uint32_t hash = 88491486UL;
|
||||
@@ -269,7 +270,7 @@ bool APIServer::check_password(const uint8_t *password_data, size_t password_len
|
||||
|
||||
void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||
|
||||
// Macro for entities without extra parameters
|
||||
// Macro for controller update dispatch
|
||||
#define API_DISPATCH_UPDATE(entity_type, entity_name) \
|
||||
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
if (obj->is_internal()) \
|
||||
@@ -278,15 +279,6 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||
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
|
||||
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
|
||||
#endif
|
||||
@@ -304,15 +296,15 @@ API_DISPATCH_UPDATE(light::LightState, light)
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state)
|
||||
API_DISPATCH_UPDATE(sensor::Sensor, sensor)
|
||||
#endif
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state)
|
||||
API_DISPATCH_UPDATE(switch_::Switch, switch)
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state)
|
||||
API_DISPATCH_UPDATE(text_sensor::TextSensor, text_sensor)
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
@@ -320,7 +312,7 @@ API_DISPATCH_UPDATE(climate::Climate, climate)
|
||||
#endif
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state)
|
||||
API_DISPATCH_UPDATE(number::Number, number)
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
@@ -336,11 +328,11 @@ API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state)
|
||||
API_DISPATCH_UPDATE(text::Text, text)
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index)
|
||||
API_DISPATCH_UPDATE(select::Select, select)
|
||||
#endif
|
||||
|
||||
#ifdef USE_LOCK
|
||||
@@ -356,12 +348,13 @@ API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
// Event is a special case - it's the only entity that passes extra parameters to the send method
|
||||
void APIServer::on_event(event::Event *obj, const std::string &event_type) {
|
||||
// Event is a special case - unlike other entities with simple state fields,
|
||||
// events store their state in a member accessed via obj->get_last_event_type()
|
||||
void APIServer::on_event(event::Event *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_event(obj, event_type);
|
||||
c->send_event(obj, obj->get_last_event_type());
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -72,19 +72,19 @@ class APIServer : public Component, public Controller {
|
||||
void on_light_update(light::LightState *obj) override;
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
void on_sensor_update(sensor::Sensor *obj, float state) override;
|
||||
void on_sensor_update(sensor::Sensor *obj) override;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void on_switch_update(switch_::Switch *obj, bool state) override;
|
||||
void on_switch_update(switch_::Switch *obj) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override;
|
||||
void on_text_sensor_update(text_sensor::TextSensor *obj) override;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
void on_climate_update(climate::Climate *obj) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void on_number_update(number::Number *obj, float state) override;
|
||||
void on_number_update(number::Number *obj) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void on_date_update(datetime::DateEntity *obj) override;
|
||||
@@ -96,10 +96,10 @@ class APIServer : public Component, public Controller {
|
||||
void on_datetime_update(datetime::DateTimeEntity *obj) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void on_text_update(text::Text *obj, const std::string &state) override;
|
||||
void on_text_update(text::Text *obj) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
|
||||
void on_select_update(select::Select *obj) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void on_lock_update(lock::Lock *obj) override;
|
||||
@@ -141,7 +141,7 @@ class APIServer : public Component, public Controller {
|
||||
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override;
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
void on_event(event::Event *obj, const std::string &event_type) override;
|
||||
void on_event(event::Event *obj) override;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
void on_update(update::UpdateEntity *obj) override;
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
|
||||
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceDynamic<Ts...> {
|
||||
public:
|
||||
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
|
||||
void (T::*callback)(Ts...))
|
||||
: UserServiceBase<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
|
||||
: UserServiceDynamic<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
|
||||
|
||||
protected:
|
||||
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
|
||||
|
||||
@@ -23,11 +23,13 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
|
||||
|
||||
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 {
|
||||
public:
|
||||
UserServiceBase(std::string name, const std::array<std::string, sizeof...(Ts)> &arg_names)
|
||||
: name_(std::move(name)), arg_names_(arg_names) {
|
||||
this->key_ = fnv1_hash(this->name_);
|
||||
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: name_(name), arg_names_(arg_names) {
|
||||
this->key_ = fnv1_hash(name);
|
||||
}
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
@@ -47,7 +49,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
bool execute_service(const ExecuteServiceRequest &req) override {
|
||||
if (req.key != this->key_)
|
||||
return false;
|
||||
if (req.args.size() != this->arg_names_.size())
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
return true;
|
||||
@@ -59,14 +61,60 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
std::string name_;
|
||||
// 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) {
|
||||
this->key_ = fnv1_hash(this->name_.c_str());
|
||||
}
|
||||
|
||||
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]))...);
|
||||
}
|
||||
|
||||
// Heap-allocated strings for runtime-generated names
|
||||
std::string name_;
|
||||
std::array<std::string, sizeof...(Ts)> arg_names_;
|
||||
uint32_t key_{0};
|
||||
};
|
||||
|
||||
template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names)
|
||||
// Constructor for static names (YAML-defined services - used by code generator)
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names) {}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "binary_sensor.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -37,6 +39,9 @@ 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
|
||||
if (this->set_state_(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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "climate.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/macros.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -463,6 +465,9 @@ void Climate::publish_state() {
|
||||
|
||||
// Send state to frontend
|
||||
this->state_callback_.call(*this);
|
||||
#if defined(USE_CLIMATE) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_climate_update(this);
|
||||
#endif
|
||||
// Save state
|
||||
this->save_state_();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ BYTE_ORDER_BIG = "big_endian"
|
||||
CONF_COLOR_DEPTH = "color_depth"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
||||
CONF_ON_RECEIVE = "on_receive"
|
||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||
CONF_REQUEST_HEADERS = "request_headers"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include "cover.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
|
||||
#include <strings.h>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -169,6 +173,9 @@ void Cover::publish_state(bool save) {
|
||||
ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation));
|
||||
|
||||
this->state_callback_.call();
|
||||
#if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_cover_update(this);
|
||||
#endif
|
||||
|
||||
if (save) {
|
||||
CoverRestoreState restore{};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "date_entity.h"
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#ifdef USE_DATETIME_DATE
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
@@ -32,6 +33,9 @@ void DateEntity::publish_state() {
|
||||
this->set_has_state(true);
|
||||
ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
|
||||
this->state_callback_.call();
|
||||
#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_date_update(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
DateCall DateEntity::make_call() { return DateCall(this); }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "datetime_entity.h"
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
@@ -48,6 +49,9 @@ void DateTimeEntity::publish_state() {
|
||||
ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_,
|
||||
this->month_, this->day_, this->hour_, this->minute_, this->second_);
|
||||
this->state_callback_.call();
|
||||
#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_datetime_update(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "time_entity.h"
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#ifdef USE_DATETIME_TIME
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
@@ -29,6 +30,9 @@ void TimeEntity::publish_state() {
|
||||
ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_,
|
||||
this->second_);
|
||||
this->state_callback_.call();
|
||||
#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_time_update(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
TimeCall TimeEntity::make_call() { return TimeCall(this); }
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace demo {
|
||||
|
||||
class DemoSelect : public select::Select, public Component {
|
||||
protected:
|
||||
void control(const std::string &value) override { this->publish_state(value); }
|
||||
void control(size_t index) override { this->publish_state(index); }
|
||||
};
|
||||
|
||||
} // namespace demo
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
namespace esphome {
|
||||
namespace es8388 {
|
||||
|
||||
void ADCInputMicSelect::control(const std::string &value) {
|
||||
this->publish_state(value);
|
||||
this->parent_->set_adc_input_mic(static_cast<AdcInputMicLine>(this->index_of(value).value()));
|
||||
void ADCInputMicSelect::control(size_t index) {
|
||||
this->publish_state(index);
|
||||
this->parent_->set_adc_input_mic(static_cast<AdcInputMicLine>(index));
|
||||
}
|
||||
|
||||
} // namespace es8388
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace es8388 {
|
||||
|
||||
class ADCInputMicSelect : public select::Select, public Parented<ES8388> {
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
void control(size_t index) override;
|
||||
};
|
||||
|
||||
} // namespace es8388
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
namespace esphome {
|
||||
namespace es8388 {
|
||||
|
||||
void DacOutputSelect::control(const std::string &value) {
|
||||
this->publish_state(value);
|
||||
this->parent_->set_dac_output(static_cast<DacOutputLine>(this->index_of(value).value()));
|
||||
void DacOutputSelect::control(size_t index) {
|
||||
this->publish_state(index);
|
||||
this->parent_->set_dac_output(static_cast<DacOutputLine>(index));
|
||||
}
|
||||
|
||||
} // namespace es8388
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace es8388 {
|
||||
|
||||
class DacOutputSelect : public select::Select, public Parented<ES8388> {
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
void control(size_t index) override;
|
||||
};
|
||||
|
||||
} // namespace es8388
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "event.h"
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -8,11 +9,11 @@ namespace event {
|
||||
static const char *const TAG = "event";
|
||||
|
||||
void Event::trigger(const std::string &event_type) {
|
||||
// Linear search - faster than std::set for small datasets (1-5 items typical)
|
||||
const std::string *found = nullptr;
|
||||
for (const auto &type : this->types_) {
|
||||
if (type == event_type) {
|
||||
found = &type;
|
||||
// Linear search with strcmp - faster than std::set for small datasets (1-5 items typical)
|
||||
const char *found = nullptr;
|
||||
for (const char *type : this->types_) {
|
||||
if (strcmp(type, event_type.c_str()) == 0) {
|
||||
found = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -20,9 +21,28 @@ void Event::trigger(const std::string &event_type) {
|
||||
ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str());
|
||||
return;
|
||||
}
|
||||
last_event_type = found;
|
||||
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str());
|
||||
this->last_event_type_ = found;
|
||||
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_);
|
||||
this->event_callback_.call(event_type);
|
||||
#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_event(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Event::set_event_types(const FixedVector<const char *> &event_types) {
|
||||
this->types_.init(event_types.size());
|
||||
for (const char *type : event_types) {
|
||||
this->types_.push_back(type);
|
||||
}
|
||||
this->last_event_type_ = nullptr; // Reset when types change
|
||||
}
|
||||
|
||||
void Event::set_event_types(const std::vector<const char *> &event_types) {
|
||||
this->types_.init(event_types.size());
|
||||
for (const char *type : event_types) {
|
||||
this->types_.push_back(type);
|
||||
}
|
||||
this->last_event_type_ = nullptr; // Reset when types change
|
||||
}
|
||||
|
||||
void Event::add_on_event_callback(std::function<void(const std::string &event_type)> &&callback) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
@@ -22,16 +24,39 @@ namespace event {
|
||||
|
||||
class Event : public EntityBase, public EntityBase_DeviceClass {
|
||||
public:
|
||||
const std::string *last_event_type;
|
||||
|
||||
void trigger(const std::string &event_type);
|
||||
void set_event_types(const std::initializer_list<std::string> &event_types) { this->types_ = event_types; }
|
||||
const FixedVector<std::string> &get_event_types() const { return this->types_; }
|
||||
|
||||
/// Set the event types supported by this event (from initializer list).
|
||||
void set_event_types(std::initializer_list<const char *> event_types) {
|
||||
this->types_ = event_types;
|
||||
this->last_event_type_ = nullptr; // Reset when types change
|
||||
}
|
||||
/// Set the event types supported by this event (from FixedVector).
|
||||
void set_event_types(const FixedVector<const char *> &event_types);
|
||||
/// Set the event types supported by this event (from vector).
|
||||
void set_event_types(const std::vector<const char *> &event_types);
|
||||
|
||||
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
|
||||
void set_event_types(std::initializer_list<std::string> event_types) = delete;
|
||||
void set_event_types(const FixedVector<std::string> &event_types) = delete;
|
||||
void set_event_types(const std::vector<std::string> &event_types) = delete;
|
||||
|
||||
/// Return the event types supported by this event.
|
||||
const FixedVector<const char *> &get_event_types() const { return this->types_; }
|
||||
|
||||
/// Return the last triggered event type (pointer to string in types_), or nullptr if no event triggered yet.
|
||||
const char *get_last_event_type() const { return this->last_event_type_; }
|
||||
|
||||
void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback);
|
||||
|
||||
protected:
|
||||
CallbackManager<void(const std::string &event_type)> event_callback_;
|
||||
FixedVector<std::string> types_;
|
||||
FixedVector<const char *> types_;
|
||||
|
||||
private:
|
||||
/// Last triggered event type - must point to entry in types_ to ensure valid lifetime.
|
||||
/// Set by trigger() after validation, reset to nullptr when types_ changes.
|
||||
const char *last_event_type_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace event
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "fan.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -181,6 +183,9 @@ void Fan::publish_state() {
|
||||
ESP_LOGD(TAG, " Preset Mode: %s", preset);
|
||||
}
|
||||
this->state_callback_.call();
|
||||
#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_fan_update(this);
|
||||
#endif
|
||||
this->save_state_();
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ void GDK101Component::dump_config() {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
#ifdef USE_SENSOR
|
||||
LOG_SENSOR(" ", "Firmware Version", this->fw_version_sensor_);
|
||||
LOG_SENSOR(" ", "Average Radaition Dose per 1 minute", this->rad_1m_sensor_);
|
||||
LOG_SENSOR(" ", "Average Radaition Dose per 10 minutes", this->rad_10m_sensor_);
|
||||
LOG_SENSOR(" ", "Status", this->status_sensor_);
|
||||
@@ -72,6 +71,10 @@ void GDK101Component::dump_config() {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Vibration Status", this->vibration_binary_sensor_);
|
||||
#endif // USE_BINARY_SENSOR
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
LOG_TEXT_SENSOR(" ", "Firmware Version", this->fw_version_text_sensor_);
|
||||
#endif // USE_TEXT_SENSOR
|
||||
}
|
||||
|
||||
float GDK101Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
@@ -153,18 +156,18 @@ bool GDK101Component::read_status_(uint8_t *data) {
|
||||
}
|
||||
|
||||
bool GDK101Component::read_fw_version_(uint8_t *data) {
|
||||
#ifdef USE_SENSOR
|
||||
if (this->fw_version_sensor_ != nullptr) {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->fw_version_text_sensor_ != nullptr) {
|
||||
if (!this->read_bytes(GDK101_REG_READ_FIRMWARE, data, 2)) {
|
||||
ESP_LOGE(TAG, "Updating GDK101 failed!");
|
||||
return false;
|
||||
}
|
||||
|
||||
const float fw_version = data[0] + (data[1] / 10.0f);
|
||||
const std::string fw_version_str = str_sprintf("%d.%d", data[0], data[1]);
|
||||
|
||||
this->fw_version_sensor_->publish_state(fw_version);
|
||||
this->fw_version_text_sensor_->publish_state(fw_version_str);
|
||||
}
|
||||
#endif // USE_SENSOR
|
||||
#endif // USE_TEXT_SENSOR
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif // USE_BINARY_SENSOR
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif // USE_TEXT_SENSOR
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -25,12 +28,14 @@ class GDK101Component : public PollingComponent, public i2c::I2CDevice {
|
||||
SUB_SENSOR(rad_1m)
|
||||
SUB_SENSOR(rad_10m)
|
||||
SUB_SENSOR(status)
|
||||
SUB_SENSOR(fw_version)
|
||||
SUB_SENSOR(measurement_duration)
|
||||
#endif // USE_SENSOR
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
SUB_BINARY_SENSOR(vibration)
|
||||
#endif // USE_BINARY_SENSOR
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
SUB_TEXT_SENSOR(fw_version)
|
||||
#endif // USE_TEXT_SENSOR
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
@@ -40,9 +40,8 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
device_class=DEVICE_CLASS_EMPTY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_VERSION): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
accuracy_decimals=1,
|
||||
cv.Optional(CONF_VERSION): cv.invalid(
|
||||
"The 'version' option has been moved to the `text_sensor` component."
|
||||
),
|
||||
cv.Optional(CONF_STATUS): sensor.sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
@@ -71,10 +70,6 @@ async def to_code(config):
|
||||
sens = await sensor.new_sensor(radiation_dose_per_10m)
|
||||
cg.add(hub.set_rad_10m_sensor(sens))
|
||||
|
||||
if version_config := config.get(CONF_VERSION):
|
||||
sens = await sensor.new_sensor(version_config)
|
||||
cg.add(hub.set_fw_version_sensor(sens))
|
||||
|
||||
if status_config := config.get(CONF_STATUS):
|
||||
sens = await sensor.new_sensor(status_config)
|
||||
cg.add(hub.set_status_sensor(sens))
|
||||
|
||||
23
esphome/components/gdk101/text_sensor.py
Normal file
23
esphome/components/gdk101/text_sensor.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP
|
||||
|
||||
from . import CONF_GDK101_ID, GDK101Component
|
||||
|
||||
DEPENDENCIES = ["gdk101"]
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_GDK101_ID): cv.use_id(GDK101Component),
|
||||
cv.Required(CONF_VERSION): text_sensor.text_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_GDK101_ID])
|
||||
var = await text_sensor.new_text_sensor(config[CONF_VERSION])
|
||||
cg.add(hub.set_fw_version_text_sensor(var))
|
||||
@@ -235,7 +235,7 @@ void GraphLegend::init(Graph *g) {
|
||||
std::string valstr =
|
||||
value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
|
||||
if (this->units_) {
|
||||
valstr += trace->sensor_->get_unit_of_measurement();
|
||||
valstr += trace->sensor_->get_unit_of_measurement_ref();
|
||||
}
|
||||
this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh);
|
||||
if (fw > valw)
|
||||
@@ -371,7 +371,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of
|
||||
std::string valstr =
|
||||
value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
|
||||
if (legend_->units_) {
|
||||
valstr += trace->sensor_->get_unit_of_measurement();
|
||||
valstr += trace->sensor_->get_unit_of_measurement_ref();
|
||||
}
|
||||
buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str());
|
||||
ESP_LOGV(TAG, " value: %s", valstr.c_str());
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "light_output.h"
|
||||
#include "light_state.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "light_output.h"
|
||||
#include "transformers.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -137,7 +138,12 @@ void LightState::loop() {
|
||||
|
||||
float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
|
||||
|
||||
void LightState::publish_state() { this->remote_values_callback_.call(); }
|
||||
void LightState::publish_state() {
|
||||
this->remote_values_callback_.call();
|
||||
#if defined(USE_LIGHT) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_light_update(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
LightOutput *LightState::get_output() const { return this->output_; }
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "lock.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -53,6 +55,9 @@ void Lock::publish_state(LockState state) {
|
||||
this->rtc_.save(&this->state);
|
||||
ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), lock_state_to_string(state));
|
||||
this->state_callback_.call();
|
||||
#if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_lock_update(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Lock::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
|
||||
|
||||
@@ -3,6 +3,8 @@ import re
|
||||
from esphome import config_validation as cv
|
||||
from esphome.const import CONF_ARGS, CONF_FORMAT
|
||||
|
||||
CONF_IF_NAN = "if_nan"
|
||||
|
||||
lv_uses = {
|
||||
"USER_DATA",
|
||||
"LOG",
|
||||
@@ -21,23 +23,48 @@ lv_fonts_used = set()
|
||||
esphome_fonts_used = set()
|
||||
lvgl_components_required = set()
|
||||
|
||||
|
||||
def validate_printf(value):
|
||||
cfmt = r"""
|
||||
# noqa
|
||||
f_regex = re.compile(
|
||||
r"""
|
||||
( # start of capture group 1
|
||||
% # literal "%"
|
||||
(?:[-+0 #]{0,5}) # optional flags
|
||||
[-+0 #]{0,5} # optional flags
|
||||
(?:\d+|\*)? # width
|
||||
(?:\.(?:\d+|\*))? # precision
|
||||
(?:h|l|ll|w|I|I32|I64)? # size
|
||||
f # type
|
||||
)
|
||||
""",
|
||||
flags=re.VERBOSE,
|
||||
)
|
||||
# noqa
|
||||
c_regex = re.compile(
|
||||
r"""
|
||||
( # start of capture group 1
|
||||
% # literal "%"
|
||||
[-+0 #]{0,5} # optional flags
|
||||
(?:\d+|\*)? # width
|
||||
(?:\.(?:\d+|\*))? # precision
|
||||
(?:h|l|ll|w|I|I32|I64)? # size
|
||||
[cCdiouxXeEfgGaAnpsSZ] # type
|
||||
)
|
||||
""" # noqa
|
||||
matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE)
|
||||
""",
|
||||
flags=re.VERBOSE,
|
||||
)
|
||||
|
||||
|
||||
def validate_printf(value):
|
||||
format_string = value[CONF_FORMAT]
|
||||
matches = c_regex.findall(format_string)
|
||||
if len(matches) != len(value[CONF_ARGS]):
|
||||
raise cv.Invalid(
|
||||
f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!"
|
||||
)
|
||||
|
||||
if value.get(CONF_IF_NAN) and len(f_regex.findall(format_string)) != 1:
|
||||
raise cv.Invalid(
|
||||
"Use of 'if_nan' requires a single valid printf-pattern of type %f"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,13 @@ from .defines import (
|
||||
call_lambda,
|
||||
literal,
|
||||
)
|
||||
from .helpers import add_lv_use, esphome_fonts_used, lv_fonts_used, requires_component
|
||||
from .helpers import (
|
||||
CONF_IF_NAN,
|
||||
add_lv_use,
|
||||
esphome_fonts_used,
|
||||
lv_fonts_used,
|
||||
requires_component,
|
||||
)
|
||||
from .types import lv_font_t, lv_gradient_t
|
||||
|
||||
opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
|
||||
@@ -412,7 +418,13 @@ class TextValidator(LValidator):
|
||||
str_args = [str(x) for x in value[CONF_ARGS]]
|
||||
arg_expr = cg.RawExpression(",".join(str_args))
|
||||
format_str = cpp_string_escape(format_str)
|
||||
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()")
|
||||
sprintf_str = f"str_sprintf({format_str}, {arg_expr}).c_str()"
|
||||
if nanval := value.get(CONF_IF_NAN):
|
||||
nanval = cpp_string_escape(nanval)
|
||||
return literal(
|
||||
f"(std::isfinite({arg_expr}) ? {sprintf_str} : {nanval})"
|
||||
)
|
||||
return literal(sprintf_str)
|
||||
if time_format := value.get(CONF_TIME_FORMAT):
|
||||
source = value[CONF_TIME]
|
||||
if isinstance(source, Lambda):
|
||||
|
||||
@@ -20,7 +20,7 @@ from esphome.core.config import StartupTrigger
|
||||
|
||||
from . import defines as df, lv_validation as lvalid
|
||||
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
|
||||
from .helpers import requires_component, validate_printf
|
||||
from .helpers import CONF_IF_NAN, requires_component, validate_printf
|
||||
from .layout import (
|
||||
FLEX_OBJ_SCHEMA,
|
||||
GRID_CELL_SCHEMA,
|
||||
@@ -54,6 +54,7 @@ PRINTF_TEXT_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_FORMAT): cv.string,
|
||||
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
|
||||
cv.Optional(CONF_IF_NAN): cv.string,
|
||||
},
|
||||
),
|
||||
validate_printf,
|
||||
|
||||
@@ -41,16 +41,16 @@ class LVGLSelect : public select::Select, public Component {
|
||||
}
|
||||
|
||||
void publish() {
|
||||
this->publish_state(this->widget_->get_selected_text());
|
||||
auto index = this->widget_->get_selected_index();
|
||||
this->publish_state(index);
|
||||
if (this->restore_) {
|
||||
auto index = this->widget_->get_selected_index();
|
||||
this->pref_.save(&index);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override {
|
||||
this->widget_->set_selected_text(value, this->anim_);
|
||||
void control(size_t index) override {
|
||||
this->widget_->set_selected_index(index, this->anim_);
|
||||
this->publish();
|
||||
}
|
||||
void set_options_() {
|
||||
|
||||
1
esphome/components/mcp3221/__init__.py
Normal file
1
esphome/components/mcp3221/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@philippderdiedas"]
|
||||
31
esphome/components/mcp3221/mcp3221_sensor.cpp
Normal file
31
esphome/components/mcp3221/mcp3221_sensor.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "mcp3221_sensor.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace mcp3221 {
|
||||
|
||||
static const char *const TAG = "mcp3221";
|
||||
|
||||
float MCP3221Sensor::sample() {
|
||||
uint8_t data[2];
|
||||
if (this->read(data, 2) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Read failed");
|
||||
this->status_set_warning();
|
||||
return NAN;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
|
||||
uint16_t value = encode_uint16(data[0], data[1]);
|
||||
float voltage = value * this->reference_voltage_ / 4096.0f;
|
||||
|
||||
return voltage;
|
||||
}
|
||||
|
||||
void MCP3221Sensor::update() {
|
||||
float v = this->sample();
|
||||
this->publish_state(v);
|
||||
}
|
||||
|
||||
} // namespace mcp3221
|
||||
} // namespace esphome
|
||||
28
esphome/components/mcp3221/mcp3221_sensor.h
Normal file
28
esphome/components/mcp3221/mcp3221_sensor.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/voltage_sampler/voltage_sampler.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace mcp3221 {
|
||||
|
||||
class MCP3221Sensor : public sensor::Sensor,
|
||||
public PollingComponent,
|
||||
public voltage_sampler::VoltageSampler,
|
||||
public i2c::I2CDevice {
|
||||
public:
|
||||
void set_reference_voltage(float reference_voltage) { this->reference_voltage_ = reference_voltage; }
|
||||
void update() override;
|
||||
float sample() override;
|
||||
|
||||
protected:
|
||||
float reference_voltage_;
|
||||
};
|
||||
|
||||
} // namespace mcp3221
|
||||
} // namespace esphome
|
||||
49
esphome/components/mcp3221/sensor.py
Normal file
49
esphome/components/mcp3221/sensor.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor, voltage_sampler
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_REFERENCE_VOLTAGE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ICON_SCALE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
|
||||
AUTO_LOAD = ["voltage_sampler"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
|
||||
mcp3221_ns = cg.esphome_ns.namespace("mcp3221")
|
||||
MCP3221Sensor = mcp3221_ns.class_(
|
||||
"MCP3221Sensor",
|
||||
sensor.Sensor,
|
||||
voltage_sampler.VoltageSampler,
|
||||
cg.PollingComponent,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
MCP3221Sensor,
|
||||
icon=ICON_SCALE,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage,
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x48))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await sensor.new_sensor(config)
|
||||
cg.add(var.set_reference_voltage(config[CONF_REFERENCE_VOLTAGE]))
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
@@ -37,8 +37,6 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
|
||||
MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
|
||||
|
||||
void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services) {
|
||||
this->hostname_ = App.get_name();
|
||||
|
||||
// IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES
|
||||
// in mdns/__init__.py. If you add a new service here, update both locations.
|
||||
|
||||
@@ -179,7 +177,7 @@ void MDNSComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"mDNS:\n"
|
||||
" Hostname: %s",
|
||||
this->hostname_.c_str());
|
||||
App.get_name().c_str());
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
ESP_LOGV(TAG, " Services:");
|
||||
for (const auto &service : this->services_) {
|
||||
|
||||
@@ -76,7 +76,6 @@ class MDNSComponent : public Component {
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
|
||||
#endif
|
||||
std::string hostname_;
|
||||
void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#if defined(USE_ESP32) && defined(USE_MDNS)
|
||||
|
||||
#include <mdns.h>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "mdns_component.h"
|
||||
@@ -27,8 +28,9 @@ void MDNSComponent::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
mdns_hostname_set(this->hostname_.c_str());
|
||||
mdns_instance_name_set(this->hostname_.c_str());
|
||||
const char *hostname = App.get_name().c_str();
|
||||
mdns_hostname_set(hostname);
|
||||
mdns_instance_name_set(hostname);
|
||||
|
||||
for (const auto &service : services) {
|
||||
auto txt_records = std::make_unique<mdns_txt_item_t[]>(service.txt_records.size());
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <ESP8266mDNS.h>
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "mdns_component.h"
|
||||
@@ -20,7 +21,7 @@ void MDNSComponent::setup() {
|
||||
this->compile_records_(services);
|
||||
#endif
|
||||
|
||||
MDNS.begin(this->hostname_.c_str());
|
||||
MDNS.begin(App.get_name().c_str());
|
||||
|
||||
for (const auto &service : services) {
|
||||
// Strip the leading underscore from the proto and service_type. While it is
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "mdns_component.h"
|
||||
|
||||
@@ -20,7 +21,7 @@ void MDNSComponent::setup() {
|
||||
this->compile_records_(services);
|
||||
#endif
|
||||
|
||||
MDNS.begin(this->hostname_.c_str());
|
||||
MDNS.begin(App.get_name().c_str());
|
||||
|
||||
for (const auto &service : services) {
|
||||
// Strip the leading underscore from the proto and service_type. While it is
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "mdns_component.h"
|
||||
|
||||
@@ -20,7 +21,7 @@ void MDNSComponent::setup() {
|
||||
this->compile_records_(services);
|
||||
#endif
|
||||
|
||||
MDNS.begin(this->hostname_.c_str());
|
||||
MDNS.begin(App.get_name().c_str());
|
||||
|
||||
for (const auto &service : services) {
|
||||
// Strip the leading underscore from the proto and service_type. While it is
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "media_player.h"
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -148,7 +149,12 @@ void MediaPlayer::add_on_state_callback(std::function<void()> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void MediaPlayer::publish_state() { this->state_callback_.call(); }
|
||||
void MediaPlayer::publish_state() {
|
||||
this->state_callback_.call();
|
||||
#if defined(USE_MEDIA_PLAYER) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_media_player_update(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace media_player
|
||||
} // namespace esphome
|
||||
|
||||
@@ -28,8 +28,9 @@ void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
|
||||
|
||||
if (map_it != this->mapping_.cend()) {
|
||||
size_t idx = std::distance(this->mapping_.cbegin(), map_it);
|
||||
new_state = std::string(this->option_at(idx));
|
||||
ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value);
|
||||
ESP_LOGV(TAG, "Found option %s for value %lld", this->option_at(idx), value);
|
||||
this->publish_state(idx);
|
||||
return;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "No option found for mapping %lld", value);
|
||||
}
|
||||
@@ -40,19 +41,16 @@ void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
}
|
||||
|
||||
void ModbusSelect::control(const std::string &value) {
|
||||
auto idx = this->index_of(value);
|
||||
if (!idx.has_value()) {
|
||||
ESP_LOGW(TAG, "Invalid option '%s'", value.c_str());
|
||||
return;
|
||||
}
|
||||
optional<int64_t> mapval = this->mapping_[idx.value()];
|
||||
ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str());
|
||||
void ModbusSelect::control(size_t index) {
|
||||
optional<int64_t> mapval = this->mapping_[index];
|
||||
const char *option = this->option_at(index);
|
||||
ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, option);
|
||||
|
||||
std::vector<uint16_t> data;
|
||||
|
||||
if (this->write_transform_func_.has_value()) {
|
||||
auto val = (*this->write_transform_func_)(this, value, *mapval, data);
|
||||
// Transform func requires string parameter for backward compatibility
|
||||
auto val = (*this->write_transform_func_)(this, std::string(option), *mapval, data);
|
||||
if (val.has_value()) {
|
||||
mapval = *val;
|
||||
ESP_LOGV(TAG, "write_lambda returned mapping value %lld", *mapval);
|
||||
@@ -85,7 +83,7 @@ void ModbusSelect::control(const std::string &value) {
|
||||
this->parent_->queue_command(write_cmd);
|
||||
|
||||
if (this->optimistic_)
|
||||
this->publish_state(value);
|
||||
this->publish_state(index);
|
||||
}
|
||||
|
||||
} // namespace modbus_controller
|
||||
|
||||
@@ -38,7 +38,7 @@ class ModbusSelect : public Component, public select::Select, public SensorItem
|
||||
|
||||
void dump_config() override;
|
||||
void parse_and_publish(const std::vector<uint8_t> &data) override;
|
||||
void control(const std::string &value) override;
|
||||
void control(size_t index) override;
|
||||
|
||||
protected:
|
||||
std::vector<int64_t> mapping_{};
|
||||
|
||||
@@ -36,7 +36,7 @@ void MQTTAlarmControlPanelComponent::setup() {
|
||||
} else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) {
|
||||
call.triggered();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name().c_str(), payload.c_str());
|
||||
ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str());
|
||||
}
|
||||
call.perform();
|
||||
});
|
||||
|
||||
@@ -30,9 +30,12 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor
|
||||
}
|
||||
|
||||
void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->binary_sensor_->get_device_class().empty())
|
||||
root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class();
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto device_class = this->binary_sensor_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
if (this->binary_sensor_->is_status_binary_sensor())
|
||||
root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available;
|
||||
if (this->binary_sensor_->is_status_binary_sensor())
|
||||
|
||||
@@ -20,7 +20,7 @@ void MQTTButtonComponent::setup() {
|
||||
if (payload == "PRESS") {
|
||||
this->button_->press();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str());
|
||||
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str());
|
||||
this->status_momentary_warning("state", 5000);
|
||||
}
|
||||
});
|
||||
@@ -33,8 +33,9 @@ void MQTTButtonComponent::dump_config() {
|
||||
void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
config.state_topic = false;
|
||||
if (!this->button_->get_device_class().empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = this->button_->get_device_class();
|
||||
const auto device_class = this->button_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
@@ -64,11 +64,11 @@ bool MQTTComponent::send_discovery_() {
|
||||
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
|
||||
|
||||
if (discovery_info.clean) {
|
||||
ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name().c_str());
|
||||
ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str());
|
||||
return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str());
|
||||
ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str());
|
||||
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return global_mqtt_client->publish_json(
|
||||
@@ -85,12 +85,16 @@ bool MQTTComponent::send_discovery_() {
|
||||
}
|
||||
|
||||
// Fields from EntityBase
|
||||
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name() : "";
|
||||
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : "";
|
||||
|
||||
if (this->is_disabled_by_default())
|
||||
if (this->is_disabled_by_default_())
|
||||
root[MQTT_ENABLED_BY_DEFAULT] = false;
|
||||
if (!this->get_icon().empty())
|
||||
root[MQTT_ICON] = this->get_icon();
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto icon_ref = this->get_icon_ref_();
|
||||
if (!icon_ref.empty()) {
|
||||
root[MQTT_ICON] = icon_ref;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
const auto entity_category = this->get_entity()->get_entity_category();
|
||||
switch (entity_category) {
|
||||
@@ -122,7 +126,7 @@ bool MQTTComponent::send_discovery_() {
|
||||
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
|
||||
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
|
||||
char friendly_name_hash[9];
|
||||
sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name()));
|
||||
sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_()));
|
||||
friendly_name_hash[8] = 0; // ensure the hash-string ends with null
|
||||
root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash;
|
||||
} else {
|
||||
@@ -184,7 +188,7 @@ bool MQTTComponent::is_discovery_enabled() const {
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_default_object_id_() const {
|
||||
return str_sanitize(str_snake_case(this->friendly_name()));
|
||||
return str_sanitize(str_snake_case(this->friendly_name_()));
|
||||
}
|
||||
|
||||
void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) {
|
||||
@@ -268,9 +272,9 @@ void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; }
|
||||
bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); }
|
||||
|
||||
// Pull these properties from EntityBase if not overridden
|
||||
std::string MQTTComponent::friendly_name() const { return this->get_entity()->get_name(); }
|
||||
std::string MQTTComponent::get_icon() const { return this->get_entity()->get_icon(); }
|
||||
bool MQTTComponent::is_disabled_by_default() const { return this->get_entity()->is_disabled_by_default(); }
|
||||
std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); }
|
||||
StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); }
|
||||
bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); }
|
||||
bool MQTTComponent::is_internal() {
|
||||
if (this->has_custom_state_topic_) {
|
||||
// If the custom state_topic is null, return true as it is internal and should not publish
|
||||
|
||||
@@ -165,13 +165,13 @@ class MQTTComponent : public Component {
|
||||
virtual const EntityBase *get_entity() const = 0;
|
||||
|
||||
/// Get the friendly name of this MQTT component.
|
||||
virtual std::string friendly_name() const;
|
||||
std::string friendly_name_() const;
|
||||
|
||||
/// Get the icon field of this component
|
||||
virtual std::string get_icon() const;
|
||||
/// Get the icon field of this component as StringRef
|
||||
StringRef get_icon_ref_() const;
|
||||
|
||||
/// Get whether the underlying Entity is disabled by default
|
||||
virtual bool is_disabled_by_default() const;
|
||||
bool is_disabled_by_default_() const;
|
||||
|
||||
/// Get the MQTT topic that new states will be shared to.
|
||||
std::string get_state_topic_() const;
|
||||
|
||||
@@ -67,9 +67,12 @@ void MQTTCoverComponent::dump_config() {
|
||||
}
|
||||
}
|
||||
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->cover_->get_device_class().empty())
|
||||
root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class();
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto device_class = this->cover_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
auto traits = this->cover_->get_traits();
|
||||
if (traits.get_is_assumed_state()) {
|
||||
|
||||
@@ -21,8 +21,12 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
|
||||
for (const auto &event_type : this->event_->get_event_types())
|
||||
event_types.add(event_type);
|
||||
|
||||
if (!this->event_->get_device_class().empty())
|
||||
root[MQTT_DEVICE_CLASS] = this->event_->get_device_class();
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto device_class = this->event_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
config.command_topic = false;
|
||||
}
|
||||
@@ -34,8 +38,8 @@ void MQTTEventComponent::setup() {
|
||||
void MQTTEventComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Event '%s': ", this->event_->get_name().c_str());
|
||||
ESP_LOGCONFIG(TAG, "Event Types: ");
|
||||
for (const auto &event_type : this->event_->get_event_types()) {
|
||||
ESP_LOGCONFIG(TAG, "- %s", event_type.c_str());
|
||||
for (const char *event_type : this->event_->get_event_types()) {
|
||||
ESP_LOGCONFIG(TAG, "- %s", event_type);
|
||||
}
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
}
|
||||
|
||||
@@ -24,15 +24,15 @@ void MQTTFanComponent::setup() {
|
||||
auto val = parse_on_off(payload.c_str());
|
||||
switch (val) {
|
||||
case PARSE_ON:
|
||||
ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name().c_str());
|
||||
ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name_().c_str());
|
||||
this->state_->turn_on().perform();
|
||||
break;
|
||||
case PARSE_OFF:
|
||||
ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name().c_str());
|
||||
ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name_().c_str());
|
||||
this->state_->turn_off().perform();
|
||||
break;
|
||||
case PARSE_TOGGLE:
|
||||
ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name().c_str());
|
||||
ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name_().c_str());
|
||||
this->state_->toggle().perform();
|
||||
break;
|
||||
case PARSE_NONE:
|
||||
@@ -48,11 +48,11 @@ void MQTTFanComponent::setup() {
|
||||
auto val = parse_on_off(payload.c_str(), "forward", "reverse");
|
||||
switch (val) {
|
||||
case PARSE_ON:
|
||||
ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name().c_str());
|
||||
ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name_().c_str());
|
||||
this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform();
|
||||
break;
|
||||
case PARSE_OFF:
|
||||
ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name().c_str());
|
||||
ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name_().c_str());
|
||||
this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform();
|
||||
break;
|
||||
case PARSE_TOGGLE:
|
||||
@@ -75,11 +75,11 @@ void MQTTFanComponent::setup() {
|
||||
auto val = parse_on_off(payload.c_str(), "oscillate_on", "oscillate_off");
|
||||
switch (val) {
|
||||
case PARSE_ON:
|
||||
ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name().c_str());
|
||||
ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name_().c_str());
|
||||
this->state_->make_call().set_oscillating(true).perform();
|
||||
break;
|
||||
case PARSE_OFF:
|
||||
ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name().c_str());
|
||||
ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name_().c_str());
|
||||
this->state_->make_call().set_oscillating(false).perform();
|
||||
break;
|
||||
case PARSE_TOGGLE:
|
||||
|
||||
@@ -24,7 +24,7 @@ void MQTTLockComponent::setup() {
|
||||
} else if (strcasecmp(payload.c_str(), "OPEN") == 0) {
|
||||
this->lock_->open();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str());
|
||||
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str());
|
||||
this->status_momentary_warning("state", 5000);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,8 +44,11 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
|
||||
root[MQTT_MIN] = traits.get_min_value();
|
||||
root[MQTT_MAX] = traits.get_max_value();
|
||||
root[MQTT_STEP] = traits.get_step();
|
||||
if (!this->number_->traits.get_unit_of_measurement().empty())
|
||||
root[MQTT_UNIT_OF_MEASUREMENT] = this->number_->traits.get_unit_of_measurement();
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref();
|
||||
if (!unit_of_measurement.empty()) {
|
||||
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
|
||||
}
|
||||
switch (this->number_->traits.get_mode()) {
|
||||
case NUMBER_MODE_AUTO:
|
||||
break;
|
||||
@@ -56,8 +59,11 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
|
||||
root[MQTT_MODE] = "slider";
|
||||
break;
|
||||
}
|
||||
if (!this->number_->traits.get_device_class().empty())
|
||||
root[MQTT_DEVICE_CLASS] = this->number_->traits.get_device_class();
|
||||
const auto device_class = this->number_->traits.get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
config.command_topic = true;
|
||||
}
|
||||
|
||||
@@ -44,13 +44,17 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire
|
||||
void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; }
|
||||
|
||||
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->sensor_->get_device_class().empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto device_class = this->sensor_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
|
||||
if (!this->sensor_->get_unit_of_measurement().empty())
|
||||
root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement();
|
||||
const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref();
|
||||
if (!unit_of_measurement.empty()) {
|
||||
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
if (this->get_expire_after() > 0)
|
||||
root[MQTT_EXPIRE_AFTER] = this->get_expire_after() / 1000;
|
||||
|
||||
@@ -29,7 +29,7 @@ void MQTTSwitchComponent::setup() {
|
||||
break;
|
||||
case PARSE_NONE:
|
||||
default:
|
||||
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str());
|
||||
ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str());
|
||||
this->status_momentary_warning("state", 5000);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@ using namespace esphome::text_sensor;
|
||||
|
||||
MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {}
|
||||
void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->sensor_->get_device_class().empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class();
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto device_class = this->sensor_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
config.command_topic = false;
|
||||
}
|
||||
void MQTTTextSensor::setup() {
|
||||
|
||||
@@ -20,7 +20,7 @@ void MQTTUpdateComponent::setup() {
|
||||
if (payload == "INSTALL") {
|
||||
this->update_->perform();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name().c_str(), payload.c_str());
|
||||
ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name_().c_str(), payload.c_str());
|
||||
this->status_momentary_warning("state", 5000);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,10 +49,12 @@ void MQTTValveComponent::dump_config() {
|
||||
}
|
||||
}
|
||||
void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (!this->valve_->get_device_class().empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class();
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto device_class = this->valve_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
|
||||
auto traits = this->valve_->get_traits();
|
||||
if (traits.get_is_assumed_state()) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "number.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -32,6 +34,9 @@ void Number::publish_state(float state) {
|
||||
this->state = state;
|
||||
ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state);
|
||||
this->state_callback_.call(state);
|
||||
#if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_number_update(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Number::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
|
||||
@@ -9,7 +9,7 @@ from esphome.components.esp32 import (
|
||||
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, TimePeriodMilliseconds
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -22,6 +22,7 @@ from .const import (
|
||||
CONF_NETWORK_KEY,
|
||||
CONF_NETWORK_NAME,
|
||||
CONF_PAN_ID,
|
||||
CONF_POLL_PERIOD,
|
||||
CONF_PSKC,
|
||||
CONF_SRP_ID,
|
||||
CONF_TLV,
|
||||
@@ -89,7 +90,7 @@ def set_sdkconfig_options(config):
|
||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True)
|
||||
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5)
|
||||
|
||||
# TODO: Add suport for sleepy end devices
|
||||
# TODO: Add suport for synchronized sleepy end devices (SSED)
|
||||
add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True)
|
||||
|
||||
|
||||
@@ -113,6 +114,17 @@ _CONNECTION_SCHEMA = cv.Schema(
|
||||
def _validate(config: ConfigType) -> ConfigType:
|
||||
if CONF_USE_ADDRESS not in config:
|
||||
config[CONF_USE_ADDRESS] = f"{CORE.name}.local"
|
||||
device_type = config.get(CONF_DEVICE_TYPE)
|
||||
poll_period = config.get(CONF_POLL_PERIOD)
|
||||
if (
|
||||
device_type == "FTD"
|
||||
and poll_period
|
||||
and poll_period > TimePeriodMilliseconds(milliseconds=0)
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"{CONF_POLL_PERIOD} can only be used with {CONF_DEVICE_TYPE}: MTD"
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -135,6 +147,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_FORCE_DATASET): cv.boolean,
|
||||
cv.Optional(CONF_TLV): cv.string_strict,
|
||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||
cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
).extend(_CONNECTION_SCHEMA),
|
||||
cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV),
|
||||
@@ -167,6 +180,8 @@ async def to_code(config):
|
||||
ot = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(ot.set_use_address(config[CONF_USE_ADDRESS]))
|
||||
await cg.register_component(ot, config)
|
||||
if (poll_period := config.get(CONF_POLL_PERIOD)) is not None:
|
||||
cg.add(ot.set_poll_period(poll_period))
|
||||
|
||||
srp = cg.new_Pvariable(config[CONF_SRP_ID])
|
||||
mdns_component = await cg.get_variable(config[CONF_MDNS_ID])
|
||||
|
||||
@@ -6,6 +6,7 @@ CONF_MESH_LOCAL_PREFIX = "mesh_local_prefix"
|
||||
CONF_NETWORK_NAME = "network_name"
|
||||
CONF_NETWORK_KEY = "network_key"
|
||||
CONF_PAN_ID = "pan_id"
|
||||
CONF_POLL_PERIOD = "poll_period"
|
||||
CONF_PSKC = "pskc"
|
||||
CONF_SRP_ID = "srp_id"
|
||||
CONF_TLV = "tlv"
|
||||
|
||||
@@ -29,6 +29,23 @@ OpenThreadComponent *global_openthread_component = // NOLINT(cppcoreguidelines-
|
||||
|
||||
OpenThreadComponent::OpenThreadComponent() { global_openthread_component = this; }
|
||||
|
||||
void OpenThreadComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Open Thread:");
|
||||
#if CONFIG_OPENTHREAD_FTD
|
||||
ESP_LOGCONFIG(TAG, " Device Type: FTD");
|
||||
#elif CONFIG_OPENTHREAD_MTD
|
||||
ESP_LOGCONFIG(TAG, " Device Type: MTD");
|
||||
// TBD: Synchronized Sleepy End Device
|
||||
if (this->poll_period > 0) {
|
||||
ESP_LOGCONFIG(TAG, " Device is configured as Sleepy End Device (SED)");
|
||||
uint32_t duration = this->poll_period / 1000;
|
||||
ESP_LOGCONFIG(TAG, " Poll Period: %" PRIu32 "s", duration);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool OpenThreadComponent::is_connected() {
|
||||
auto lock = InstanceLock::try_acquire(100);
|
||||
if (!lock) {
|
||||
|
||||
@@ -22,6 +22,7 @@ class OpenThreadComponent : public Component {
|
||||
public:
|
||||
OpenThreadComponent();
|
||||
~OpenThreadComponent();
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
bool teardown() override;
|
||||
float get_setup_priority() const override { return setup_priority::WIFI; }
|
||||
@@ -35,6 +36,9 @@ class OpenThreadComponent : public Component {
|
||||
|
||||
const char *get_use_address() const;
|
||||
void set_use_address(const char *use_address);
|
||||
#if CONFIG_OPENTHREAD_MTD
|
||||
void set_poll_period(uint32_t poll_period) { this->poll_period = poll_period; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
std::optional<otIp6Address> get_omr_address_(InstanceLock &lock);
|
||||
@@ -46,6 +50,9 @@ class OpenThreadComponent : public Component {
|
||||
// Stores a pointer to a string literal (static storage duration).
|
||||
// ONLY set from Python-generated code with string literals - never dynamic strings.
|
||||
const char *use_address_{""};
|
||||
#if CONFIG_OPENTHREAD_MTD
|
||||
uint32_t poll_period{0};
|
||||
#endif
|
||||
};
|
||||
|
||||
extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -105,6 +105,32 @@ void OpenThreadComponent::ot_main() {
|
||||
esp_cli_custom_command_init();
|
||||
#endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION
|
||||
|
||||
otLinkModeConfig link_mode_config = {0};
|
||||
#if CONFIG_OPENTHREAD_FTD
|
||||
link_mode_config.mRxOnWhenIdle = true;
|
||||
link_mode_config.mDeviceType = true;
|
||||
link_mode_config.mNetworkData = true;
|
||||
#elif CONFIG_OPENTHREAD_MTD
|
||||
if (this->poll_period > 0) {
|
||||
if (otLinkSetPollPeriod(esp_openthread_get_instance(), this->poll_period) != OT_ERROR_NONE) {
|
||||
ESP_LOGE(TAG, "Failed to set OpenThread pollperiod.");
|
||||
}
|
||||
uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance());
|
||||
ESP_LOGD(TAG, "Link Polling Period: %d", link_polling_period);
|
||||
}
|
||||
link_mode_config.mRxOnWhenIdle = this->poll_period == 0;
|
||||
link_mode_config.mDeviceType = false;
|
||||
link_mode_config.mNetworkData = false;
|
||||
#endif
|
||||
|
||||
if (otThreadSetLinkMode(esp_openthread_get_instance(), link_mode_config) != OT_ERROR_NONE) {
|
||||
ESP_LOGE(TAG, "Failed to set OpenThread linkmode.");
|
||||
}
|
||||
link_mode_config = otThreadGetLinkMode(esp_openthread_get_instance());
|
||||
ESP_LOGD(TAG, "Link Mode Device Type: %s", link_mode_config.mDeviceType ? "true" : "false");
|
||||
ESP_LOGD(TAG, "Link Mode Network Data: %s", link_mode_config.mNetworkData ? "true" : "false");
|
||||
ESP_LOGD(TAG, "Link Mode RX On When Idle: %s", link_mode_config.mRxOnWhenIdle ? "true" : "false");
|
||||
|
||||
// Run the main loop
|
||||
#if CONFIG_OPENTHREAD_CLI
|
||||
esp_openthread_cli_create_task();
|
||||
|
||||
@@ -158,7 +158,7 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor
|
||||
stream->print(ESPHOME_F("\",name=\""));
|
||||
stream->print(relabel_name_(obj).c_str());
|
||||
stream->print(ESPHOME_F("\",unit=\""));
|
||||
stream->print(obj->get_unit_of_measurement().c_str());
|
||||
stream->print(obj->get_unit_of_measurement_ref().c_str());
|
||||
stream->print(ESPHOME_F("\"} "));
|
||||
stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str());
|
||||
stream->print(ESPHOME_F("\n"));
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
import textwrap
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_IGNORE_NOT_FOUND
|
||||
from esphome.components.esp32 import (
|
||||
CONF_CPU_FREQUENCY,
|
||||
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES,
|
||||
@@ -123,6 +124,7 @@ def get_config_schema(config):
|
||||
cv.Optional(CONF_ENABLE_ECC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_SPEED, default=speeds[0]): cv.one_of(*speeds, upper=True),
|
||||
cv.Optional(CONF_DISABLED, default=False): cv.boolean,
|
||||
cv.Optional(CONF_IGNORE_NOT_FOUND, default=True): cv.boolean,
|
||||
}
|
||||
)(config)
|
||||
|
||||
@@ -147,7 +149,9 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_USE", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_USE_CAPS_ALLOC", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_IGNORE_NOTFOUND", True)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SPIRAM_IGNORE_NOTFOUND", config[CONF_IGNORE_NOT_FOUND]
|
||||
)
|
||||
|
||||
add_idf_sdkconfig_option(f"CONFIG_SPIRAM_MODE_{SDK_MODES[config[CONF_MODE]]}", True)
|
||||
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
namespace esphome {
|
||||
namespace seeed_mr24hpc1 {
|
||||
|
||||
void ExistenceBoundarySelect::control(const std::string &value) {
|
||||
this->publish_state(value);
|
||||
auto index = this->index_of(value);
|
||||
if (index.has_value()) {
|
||||
this->parent_->set_existence_boundary(index.value());
|
||||
}
|
||||
void ExistenceBoundarySelect::control(size_t index) {
|
||||
this->publish_state(index);
|
||||
this->parent_->set_existence_boundary(index);
|
||||
}
|
||||
|
||||
} // namespace seeed_mr24hpc1
|
||||
|
||||
@@ -11,7 +11,7 @@ class ExistenceBoundarySelect : public select::Select, public Parented<MR24HPC1C
|
||||
ExistenceBoundarySelect() = default;
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
void control(size_t index) override;
|
||||
};
|
||||
|
||||
} // namespace seeed_mr24hpc1
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
namespace esphome {
|
||||
namespace seeed_mr24hpc1 {
|
||||
|
||||
void MotionBoundarySelect::control(const std::string &value) {
|
||||
this->publish_state(value);
|
||||
auto index = this->index_of(value);
|
||||
if (index.has_value()) {
|
||||
this->parent_->set_motion_boundary(index.value());
|
||||
}
|
||||
void MotionBoundarySelect::control(size_t index) {
|
||||
this->publish_state(index);
|
||||
this->parent_->set_motion_boundary(index);
|
||||
}
|
||||
|
||||
} // namespace seeed_mr24hpc1
|
||||
|
||||
@@ -11,7 +11,7 @@ class MotionBoundarySelect : public select::Select, public Parented<MR24HPC1Comp
|
||||
MotionBoundarySelect() = default;
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
void control(size_t index) override;
|
||||
};
|
||||
|
||||
} // namespace seeed_mr24hpc1
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
namespace esphome {
|
||||
namespace seeed_mr24hpc1 {
|
||||
|
||||
void SceneModeSelect::control(const std::string &value) {
|
||||
this->publish_state(value);
|
||||
auto index = this->index_of(value);
|
||||
if (index.has_value()) {
|
||||
this->parent_->set_scene_mode(index.value());
|
||||
}
|
||||
void SceneModeSelect::control(size_t index) {
|
||||
this->publish_state(index);
|
||||
this->parent_->set_scene_mode(index);
|
||||
}
|
||||
|
||||
} // namespace seeed_mr24hpc1
|
||||
|
||||
@@ -11,7 +11,7 @@ class SceneModeSelect : public select::Select, public Parented<MR24HPC1Component
|
||||
SceneModeSelect() = default;
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
void control(size_t index) override;
|
||||
};
|
||||
|
||||
} // namespace seeed_mr24hpc1
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
namespace esphome {
|
||||
namespace seeed_mr24hpc1 {
|
||||
|
||||
void UnmanTimeSelect::control(const std::string &value) {
|
||||
this->publish_state(value);
|
||||
auto index = this->index_of(value);
|
||||
if (index.has_value()) {
|
||||
this->parent_->set_unman_time(index.value());
|
||||
}
|
||||
void UnmanTimeSelect::control(size_t index) {
|
||||
this->publish_state(index);
|
||||
this->parent_->set_unman_time(index);
|
||||
}
|
||||
|
||||
} // namespace seeed_mr24hpc1
|
||||
|
||||
@@ -11,7 +11,7 @@ class UnmanTimeSelect : public select::Select, public Parented<MR24HPC1Component
|
||||
UnmanTimeSelect() = default;
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
void control(size_t index) override;
|
||||
};
|
||||
|
||||
} // namespace seeed_mr24hpc1
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
namespace esphome {
|
||||
namespace seeed_mr60fda2 {
|
||||
|
||||
void HeightThresholdSelect::control(const std::string &value) {
|
||||
this->publish_state(value);
|
||||
auto index = this->index_of(value);
|
||||
if (index.has_value()) {
|
||||
this->parent_->set_height_threshold(index.value());
|
||||
}
|
||||
void HeightThresholdSelect::control(size_t index) {
|
||||
this->publish_state(index);
|
||||
this->parent_->set_height_threshold(index);
|
||||
}
|
||||
|
||||
} // namespace seeed_mr60fda2
|
||||
|
||||
@@ -11,7 +11,7 @@ class HeightThresholdSelect : public select::Select, public Parented<MR60FDA2Com
|
||||
HeightThresholdSelect() = default;
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
void control(size_t index) override;
|
||||
};
|
||||
|
||||
} // namespace seeed_mr60fda2
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
namespace esphome {
|
||||
namespace seeed_mr60fda2 {
|
||||
|
||||
void InstallHeightSelect::control(const std::string &value) {
|
||||
this->publish_state(value);
|
||||
auto index = this->index_of(value);
|
||||
if (index.has_value()) {
|
||||
this->parent_->set_install_height(index.value());
|
||||
}
|
||||
void InstallHeightSelect::control(size_t index) {
|
||||
this->publish_state(index);
|
||||
this->parent_->set_install_height(index);
|
||||
}
|
||||
|
||||
} // namespace seeed_mr60fda2
|
||||
|
||||
@@ -11,7 +11,7 @@ class InstallHeightSelect : public select::Select, public Parented<MR60FDA2Compo
|
||||
InstallHeightSelect() = default;
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
void control(size_t index) override;
|
||||
};
|
||||
|
||||
} // namespace seeed_mr60fda2
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
namespace esphome {
|
||||
namespace seeed_mr60fda2 {
|
||||
|
||||
void SensitivitySelect::control(const std::string &value) {
|
||||
this->publish_state(value);
|
||||
auto index = this->index_of(value);
|
||||
if (index.has_value()) {
|
||||
this->parent_->set_sensitivity(index.value());
|
||||
}
|
||||
void SensitivitySelect::control(size_t index) {
|
||||
this->publish_state(index);
|
||||
this->parent_->set_sensitivity(index);
|
||||
}
|
||||
|
||||
} // namespace seeed_mr60fda2
|
||||
|
||||
@@ -11,7 +11,7 @@ class SensitivitySelect : public select::Select, public Parented<MR60FDA2Compone
|
||||
SensitivitySelect() = default;
|
||||
|
||||
protected:
|
||||
void control(const std::string &value) override;
|
||||
void control(size_t index) override;
|
||||
};
|
||||
|
||||
} // namespace seeed_mr60fda2
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "select.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cstring>
|
||||
|
||||
@@ -33,6 +35,9 @@ void Select::publish_state(size_t index) {
|
||||
ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index);
|
||||
// Callback signature requires std::string, create temporary for compatibility
|
||||
this->state_callback_.call(std::string(option), index);
|
||||
#if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_select_update(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; }
|
||||
|
||||
@@ -35,7 +35,7 @@ class Select : public EntityBase {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
/// @deprecated Use current_option() instead. This member will be removed in ESPHome 2026.5.0.
|
||||
__attribute__((deprecated("Use current_option() instead of .state. Will be removed in 2026.5.0")))
|
||||
ESPDEPRECATED("Use current_option() instead of .state. Will be removed in 2026.5.0", "2025.11.0")
|
||||
std::string state{};
|
||||
|
||||
Select() = default;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "sensor.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -131,6 +133,9 @@ void Sensor::internal_send_state_to_frontend(float state) {
|
||||
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state,
|
||||
this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals());
|
||||
this->callback_.call(state);
|
||||
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_sensor_update(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace sensor
|
||||
|
||||
@@ -172,16 +172,7 @@ class LWIPRawImpl : public Socket {
|
||||
errno = ECONNRESET;
|
||||
return "";
|
||||
}
|
||||
char buffer[50] = {};
|
||||
if (IP_IS_V4_VAL(pcb_->remote_ip)) {
|
||||
inet_ntoa_r(pcb_->remote_ip, buffer, sizeof(buffer));
|
||||
}
|
||||
#if LWIP_IPV6
|
||||
else if (IP_IS_V6_VAL(pcb_->remote_ip)) {
|
||||
inet6_ntoa_r(pcb_->remote_ip, buffer, sizeof(buffer));
|
||||
}
|
||||
#endif
|
||||
return std::string(buffer);
|
||||
return this->format_ip_address_(pcb_->remote_ip);
|
||||
}
|
||||
int getsockname(struct sockaddr *name, socklen_t *addrlen) override {
|
||||
if (pcb_ == nullptr) {
|
||||
@@ -199,16 +190,7 @@ class LWIPRawImpl : public Socket {
|
||||
errno = ECONNRESET;
|
||||
return "";
|
||||
}
|
||||
char buffer[50] = {};
|
||||
if (IP_IS_V4_VAL(pcb_->local_ip)) {
|
||||
inet_ntoa_r(pcb_->local_ip, buffer, sizeof(buffer));
|
||||
}
|
||||
#if LWIP_IPV6
|
||||
else if (IP_IS_V6_VAL(pcb_->local_ip)) {
|
||||
inet6_ntoa_r(pcb_->local_ip, buffer, sizeof(buffer));
|
||||
}
|
||||
#endif
|
||||
return std::string(buffer);
|
||||
return this->format_ip_address_(pcb_->local_ip);
|
||||
}
|
||||
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
|
||||
if (pcb_ == nullptr) {
|
||||
@@ -499,6 +481,19 @@ class LWIPRawImpl : public Socket {
|
||||
}
|
||||
|
||||
protected:
|
||||
std::string format_ip_address_(const ip_addr_t &ip) {
|
||||
char buffer[50] = {};
|
||||
if (IP_IS_V4_VAL(ip)) {
|
||||
inet_ntoa_r(ip, buffer, sizeof(buffer));
|
||||
}
|
||||
#if LWIP_IPV6
|
||||
else if (IP_IS_V6_VAL(ip)) {
|
||||
inet6_ntoa_r(ip, buffer, sizeof(buffer));
|
||||
}
|
||||
#endif
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) {
|
||||
if (family_ == AF_INET) {
|
||||
if (*addrlen < sizeof(struct sockaddr_in)) {
|
||||
|
||||
@@ -650,7 +650,7 @@ void Sprinkler::set_valve_run_duration(const optional<size_t> valve_number, cons
|
||||
return;
|
||||
}
|
||||
auto call = this->valve_[valve_number.value()].run_duration_number->make_call();
|
||||
if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) {
|
||||
if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) {
|
||||
call.set_value(run_duration.value() / 60.0);
|
||||
} else {
|
||||
call.set_value(run_duration.value());
|
||||
@@ -732,7 +732,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) {
|
||||
return 0;
|
||||
}
|
||||
if (this->valve_[valve_number].run_duration_number != nullptr) {
|
||||
if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) {
|
||||
if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) {
|
||||
return static_cast<uint32_t>(roundf(this->valve_[valve_number].run_duration_number->state * 60));
|
||||
} else {
|
||||
return static_cast<uint32_t>(roundf(this->valve_[valve_number].run_duration_number->state));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user