mirror of
https://github.com/esphome/esphome.git
synced 2025-11-12 21:05:46 +00:00
Compare commits
71 Commits
controller
...
thermostat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eb471b316 | ||
|
|
20388ce848 | ||
|
|
9de80b635a | ||
|
|
748aee584a | ||
|
|
3cbfddcc83 | ||
|
|
1d71b6b93e | ||
|
|
398dba4fc8 | ||
|
|
298813d4fa | ||
|
|
56d141c741 | ||
|
|
47a7f729dd | ||
|
|
7806eb980f | ||
|
|
a59888224c | ||
|
|
58ad4759f0 | ||
|
|
87f79290ba | ||
|
|
9326d78439 | ||
|
|
a93887a790 | ||
|
|
d7fa131a8a | ||
|
|
79a4444928 | ||
|
|
572fae5c7d | ||
|
|
5dafaaced4 | ||
|
|
65a303d48f | ||
|
|
00c71b7236 | ||
|
|
ef04903a7a | ||
|
|
a2ec7f622c | ||
|
|
2f91e7bd47 | ||
|
|
80a7c6d3c3 | ||
|
|
7a92565a0c | ||
|
|
661920c51e | ||
|
|
a6b905e148 | ||
|
|
a6b7c1f18c | ||
|
|
7a700ca077 | ||
|
|
1539b43074 | ||
|
|
463a00b1ac | ||
|
|
82692d7053 | ||
|
|
1cccfdd2b9 | ||
|
|
855aa32f54 | ||
|
|
0f8332fe3c | ||
|
|
40e2976ba2 | ||
|
|
e46300828e | ||
|
|
8c5b964722 | ||
|
|
43eafbccb3 | ||
|
|
f32b69b8f1 | ||
|
|
2a16653642 | ||
|
|
b47e89a7d5 | ||
|
|
c17a31a8f8 | ||
|
|
fbbdad75f6 | ||
|
|
7abb6d4998 | ||
|
|
1dabe83d04 | ||
|
|
0d735dc259 | ||
|
|
7b86e1feb0 | ||
|
|
d516627957 | ||
|
|
fb1c67490a | ||
|
|
8b9600b930 | ||
|
|
cbb98c4050 | ||
|
|
e7ff56f1cd | ||
|
|
7705a5de06 | ||
|
|
77ab096b59 | ||
|
|
26a3ec41d6 | ||
|
|
3bcbfe8d97 | ||
|
|
870b2c4f84 | ||
|
|
5f9c7a70ff | ||
|
|
f7179d4255 | ||
|
|
eb0558ca3f | ||
|
|
5585355263 | ||
|
|
e468ca4881 | ||
|
|
4c078dea2c | ||
|
|
783dbd1e6b | ||
|
|
b49619d9bf | ||
|
|
a290b88cd6 | ||
|
|
b61027607f | ||
|
|
f55c872180 |
@@ -172,8 +172,7 @@ This document provides essential context for AI models interacting with this pro
|
||||
|
||||
* **C++ Class Pattern:**
|
||||
```cpp
|
||||
namespace esphome {
|
||||
namespace my_component {
|
||||
namespace esphome::my_component {
|
||||
|
||||
class MyComponent : public Component {
|
||||
public:
|
||||
@@ -189,8 +188,7 @@ This document provides essential context for AI models interacting with this pro
|
||||
int param_{0};
|
||||
};
|
||||
|
||||
} // namespace my_component
|
||||
} // namespace esphome
|
||||
} // namespace esphome::my_component
|
||||
```
|
||||
|
||||
* **Common Component Examples:**
|
||||
|
||||
@@ -21,7 +21,7 @@ permissions:
|
||||
jobs:
|
||||
request-codeowner-reviews:
|
||||
name: Run
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Request reviews from component codeowners
|
||||
|
||||
@@ -206,6 +206,7 @@ esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||
esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
esphome/components/hlk_fm22x/* @OnFreund
|
||||
esphome/components/hm3301/* @freekode
|
||||
esphome/components/hmac_md5/* @dwmw2
|
||||
esphome/components/homeassistant/* @esphome/core @OttoWinter
|
||||
@@ -395,6 +396,7 @@ esphome/components/rpi_dpi_rgb/* @clydebarrow
|
||||
esphome/components/rtl87xx/* @kuba2k2
|
||||
esphome/components/rtttl/* @glmnet
|
||||
esphome/components/runtime_stats/* @bdraco
|
||||
esphome/components/rx8130/* @beormund
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
esphome/components/scd4x/* @martgras @sjtrny
|
||||
esphome/components/script/* @esphome/core
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.11.0-dev
|
||||
PROJECT_NUMBER = 2025.11.0b1
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
# 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
|
||||
@@ -1,845 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,75 +0,0 @@
|
||||
# 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
|
||||
@@ -1,256 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,118 +0,0 @@
|
||||
# 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.
|
||||
@@ -227,6 +227,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
esp32=8, # More RAM, can buffer more
|
||||
rp2040=5, # Limited RAM
|
||||
bk72xx=8, # Moderate RAM
|
||||
nrf52=8, # Moderate RAM
|
||||
rtl87xx=8, # Moderate RAM
|
||||
host=16, # Abundant resources
|
||||
ln882x=8, # Moderate RAM
|
||||
|
||||
@@ -1467,6 +1467,8 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Beken");
|
||||
#elif defined(USE_LN882X)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning");
|
||||
#elif defined(USE_NRF52)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor");
|
||||
#elif defined(USE_RTL87XX)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek");
|
||||
#elif defined(USE_HOST)
|
||||
|
||||
@@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number";
|
||||
void CalibrationNumber::setup() {
|
||||
float value = 0.0f;
|
||||
if (this->restore_value_) {
|
||||
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
|
||||
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
|
||||
if (!this->pref_.load(&value)) {
|
||||
value = 0.0f;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_VALUE,
|
||||
)
|
||||
from esphome.core import ID
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_client"]
|
||||
CODEOWNERS = ["@buxtronix", "@clydebarrow"]
|
||||
@@ -198,7 +199,12 @@ async def ble_write_to_code(config, action_id, template_arg, args):
|
||||
templ = await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_value_template(templ))
|
||||
else:
|
||||
cg.add(var.set_value_simple(value))
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
if isinstance(value, bytes):
|
||||
value = list(value)
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*value))
|
||||
cg.add(var.set_value_simple(arr, len(value)))
|
||||
|
||||
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
|
||||
cg.add(
|
||||
|
||||
@@ -96,11 +96,8 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
BLEClientWriteAction(BLEClient *ble_client) {
|
||||
ble_client->register_ble_node(this);
|
||||
ble_client_ = ble_client;
|
||||
this->construct_simple_value_();
|
||||
}
|
||||
|
||||
~BLEClientWriteAction() { this->destroy_simple_value_(); }
|
||||
|
||||
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
@@ -110,17 +107,14 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
|
||||
void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
this->destroy_simple_value_();
|
||||
this->value_.template_func = func;
|
||||
this->has_simple_value_ = false;
|
||||
this->value_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
|
||||
void set_value_simple(const std::vector<uint8_t> &value) {
|
||||
if (!this->has_simple_value_) {
|
||||
this->construct_simple_value_();
|
||||
}
|
||||
this->value_.simple = value;
|
||||
this->has_simple_value_ = true;
|
||||
// Store pointer to static data in flash (no RAM copy)
|
||||
void set_value_simple(const uint8_t *data, size_t len) {
|
||||
this->value_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {}
|
||||
@@ -128,7 +122,14 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
void play_complex(const Ts &...x) override {
|
||||
this->num_running_++;
|
||||
this->var_ = std::make_tuple(x...);
|
||||
auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
|
||||
std::vector<uint8_t> value;
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: copy from flash to vector
|
||||
value.assign(this->value_.data, this->value_.data + this->len_);
|
||||
} else {
|
||||
// Template mode: call function
|
||||
value = this->value_.func(x...);
|
||||
}
|
||||
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
||||
if (!write(value))
|
||||
this->play_next_(x...);
|
||||
@@ -201,21 +202,11 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
}
|
||||
|
||||
private:
|
||||
void construct_simple_value_() { new (&this->value_.simple) std::vector<uint8_t>(); }
|
||||
|
||||
void destroy_simple_value_() {
|
||||
if (this->has_simple_value_) {
|
||||
this->value_.simple.~vector();
|
||||
}
|
||||
}
|
||||
|
||||
BLEClient *ble_client_;
|
||||
bool has_simple_value_ = true;
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Value {
|
||||
std::vector<uint8_t> simple;
|
||||
std::vector<uint8_t> (*template_func)(Ts...);
|
||||
Value() {} // trivial constructor
|
||||
~Value() {} // trivial destructor - we manage lifetime via discriminator
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} value_;
|
||||
espbt::ESPBTUUID service_uuid_;
|
||||
espbt::ESPBTUUID char_uuid_;
|
||||
|
||||
@@ -4,7 +4,7 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, ID
|
||||
|
||||
CODEOWNERS = ["@mvturnho", "@danielschramm"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -176,5 +176,8 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
|
||||
else:
|
||||
if isinstance(data, bytes):
|
||||
data = [int(x) for x in data]
|
||||
cg.add(var.set_data_static(data))
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
||||
cg.add(var.set_data_static(arr, len(data)))
|
||||
return var
|
||||
|
||||
@@ -112,13 +112,16 @@ class Canbus : public Component {
|
||||
|
||||
template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public Parented<Canbus> {
|
||||
public:
|
||||
void set_data_template(const std::function<std::vector<uint8_t>(Ts...)> func) {
|
||||
this->data_func_ = func;
|
||||
this->static_ = false;
|
||||
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
// Stateless lambdas (generated by ESPHome) implicitly convert to function pointers
|
||||
this->data_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
void set_data_static(const std::vector<uint8_t> &data) {
|
||||
this->data_static_ = data;
|
||||
this->static_ = true;
|
||||
|
||||
// Store pointer to static data in flash (no RAM copy)
|
||||
void set_data_static(const uint8_t *data, size_t len) {
|
||||
this->data_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
|
||||
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
|
||||
@@ -133,21 +136,26 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P
|
||||
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
|
||||
auto use_extended_id =
|
||||
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;
|
||||
if (this->static_) {
|
||||
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, this->data_static_);
|
||||
std::vector<uint8_t> data;
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: copy from flash to vector
|
||||
data.assign(this->data_.data, this->data_.data + this->len_);
|
||||
} else {
|
||||
auto val = this->data_func_(x...);
|
||||
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, val);
|
||||
// Template mode: call function
|
||||
data = this->data_.func(x...);
|
||||
}
|
||||
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data);
|
||||
}
|
||||
|
||||
protected:
|
||||
optional<uint32_t> can_id_{};
|
||||
optional<bool> use_extended_id_{};
|
||||
bool remote_transmission_request_{false};
|
||||
bool static_{false};
|
||||
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||
std::vector<uint8_t> data_static_{};
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Data {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} data_;
|
||||
};
|
||||
|
||||
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {
|
||||
|
||||
@@ -59,6 +59,7 @@ async def to_code(config):
|
||||
zephyr_add_prj_conf("SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL", True)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
cg.add_define("USE_DEBUG")
|
||||
|
||||
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
|
||||
@@ -49,9 +49,9 @@ void DebugComponent::dump_config() {
|
||||
}
|
||||
#endif // USE_TEXT_SENSOR
|
||||
|
||||
#ifdef USE_ESP32
|
||||
this->log_partition_info_(); // Log partition information for ESP32
|
||||
#endif // USE_ESP32
|
||||
#if defined(USE_ESP32) || defined(USE_ZEPHYR)
|
||||
this->log_partition_info_(); // Log partition information
|
||||
#endif
|
||||
}
|
||||
|
||||
void DebugComponent::loop() {
|
||||
|
||||
@@ -62,19 +62,19 @@ class DebugComponent : public PollingComponent {
|
||||
sensor::Sensor *cpu_frequency_sensor_{nullptr};
|
||||
#endif // USE_SENSOR
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32) || defined(USE_ZEPHYR)
|
||||
/**
|
||||
* @brief Logs information about the device's partition table.
|
||||
*
|
||||
* This function iterates through the ESP32's partition table and logs details
|
||||
* This function iterates through the partition table and logs details
|
||||
* about each partition, including its name, type, subtype, starting address,
|
||||
* and size. The information is useful for diagnosing issues related to flash
|
||||
* memory or verifying the partition configuration dynamically at runtime.
|
||||
*
|
||||
* Only available when compiled for ESP32 platforms.
|
||||
* Only available when compiled for ESP32 and ZEPHYR platforms.
|
||||
*/
|
||||
void log_partition_info_();
|
||||
#endif // USE_ESP32
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
text_sensor::TextSensor *device_info_{nullptr};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <zephyr/drivers/hwinfo.h>
|
||||
#include <hal/nrf_power.h>
|
||||
#include <cstdint>
|
||||
#include <zephyr/storage/flash_map.h>
|
||||
|
||||
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
|
||||
|
||||
@@ -86,6 +87,37 @@ std::string DebugComponent::get_reset_reason_() {
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
|
||||
|
||||
static void fa_cb(const struct flash_area *fa, void *user_data) {
|
||||
#if CONFIG_FLASH_MAP_LABELS
|
||||
const char *fa_label = flash_area_label(fa);
|
||||
|
||||
if (fa_label == nullptr) {
|
||||
fa_label = "-";
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s %-24.24s 0x%-10x 0x%-12x", (int) fa->fa_id,
|
||||
sizeof(uintptr_t) * 2, (uintptr_t) fa->fa_dev, fa->fa_dev->name, fa_label, (uint32_t) fa->fa_off,
|
||||
fa->fa_size);
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s 0x%-10x 0x%-12x", (int) fa->fa_id, sizeof(uintptr_t) * 2,
|
||||
(uintptr_t) fa->fa_dev, fa->fa_dev->name, (uint32_t) fa->fa_off, fa->fa_size);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DebugComponent::log_partition_info_() {
|
||||
#if CONFIG_FLASH_MAP_LABELS
|
||||
ESP_LOGCONFIG(TAG, "ID | Device | Device Name "
|
||||
"| Label | Offset | Size");
|
||||
ESP_LOGCONFIG(TAG, "--------------------------------------------"
|
||||
"-----------------------------------------------");
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, "ID | Device | Device Name "
|
||||
"| Offset | Size");
|
||||
ESP_LOGCONFIG(TAG, "-----------------------------------------"
|
||||
"------------------------------");
|
||||
#endif
|
||||
flash_area_foreach(fa_cb, nullptr);
|
||||
}
|
||||
|
||||
void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
std::string supply = "Main supply status: ";
|
||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) {
|
||||
|
||||
@@ -23,7 +23,7 @@ void DS1307Component::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
float DS1307Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
@@ -334,12 +334,14 @@ def _is_framework_url(source: str) -> str:
|
||||
# - https://github.com/espressif/arduino-esp32/releases
|
||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(3, 3, 2),
|
||||
"latest": cv.Version(3, 3, 2),
|
||||
"dev": cv.Version(3, 3, 2),
|
||||
"latest": cv.Version(3, 3, 4),
|
||||
"dev": cv.Version(3, 3, 4),
|
||||
}
|
||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 2): cv.Version(55, 3, 31, "1"),
|
||||
cv.Version(3, 3, 1): cv.Version(55, 3, 31, "1"),
|
||||
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(3, 3, 1): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"),
|
||||
cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
|
||||
cv.Version(3, 2, 0): cv.Version(54, 3, 20),
|
||||
@@ -357,8 +359,8 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"dev": cv.Version(5, 5, 1),
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
|
||||
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
|
||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
||||
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
|
||||
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
|
||||
@@ -373,9 +375,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
# The platform-espressif32 version
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
PLATFORM_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(55, 3, 31, "1"),
|
||||
"latest": cv.Version(55, 3, 31, "1"),
|
||||
"dev": cv.Version(55, 3, 31, "1"),
|
||||
"recommended": cv.Version(55, 3, 31, "2"),
|
||||
"latest": cv.Version(55, 3, 31, "2"),
|
||||
"dev": cv.Version(55, 3, 31, "2"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -336,7 +336,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
|
||||
this->connecting_sta_ = sta;
|
||||
|
||||
wifi::global_wifi_component->set_sta(sta);
|
||||
wifi::global_wifi_component->start_connecting(sta, false);
|
||||
wifi::global_wifi_component->start_connecting(sta);
|
||||
this->set_state_(improv::STATE_PROVISIONING);
|
||||
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||
command.password.c_str());
|
||||
|
||||
@@ -418,8 +418,6 @@ void EthernetComponent::dump_config() {
|
||||
|
||||
float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; }
|
||||
|
||||
bool EthernetComponent::can_proceed() { return this->is_connected(); }
|
||||
|
||||
network::IPAddresses EthernetComponent::get_ip_addresses() {
|
||||
network::IPAddresses addresses;
|
||||
esp_netif_ip_info_t ip;
|
||||
|
||||
@@ -58,7 +58,6 @@ class EthernetComponent : public Component {
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
bool can_proceed() override;
|
||||
void on_powerdown() override { powerdown(); }
|
||||
bool is_connected();
|
||||
|
||||
|
||||
247
esphome/components/hlk_fm22x/__init__.py
Normal file
247
esphome/components/hlk_fm22x/__init__.py
Normal file
@@ -0,0 +1,247 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DIRECTION,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_ON_ENROLLMENT_DONE,
|
||||
CONF_ON_ENROLLMENT_FAILED,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@OnFreund"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
AUTO_LOAD = ["binary_sensor", "sensor", "text_sensor"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_HLK_FM22X_ID = "hlk_fm22x_id"
|
||||
CONF_FACE_ID = "face_id"
|
||||
CONF_ON_FACE_SCAN_MATCHED = "on_face_scan_matched"
|
||||
CONF_ON_FACE_SCAN_UNMATCHED = "on_face_scan_unmatched"
|
||||
CONF_ON_FACE_SCAN_INVALID = "on_face_scan_invalid"
|
||||
CONF_ON_FACE_INFO = "on_face_info"
|
||||
|
||||
hlk_fm22x_ns = cg.esphome_ns.namespace("hlk_fm22x")
|
||||
HlkFm22xComponent = hlk_fm22x_ns.class_(
|
||||
"HlkFm22xComponent", cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
|
||||
FaceScanMatchedTrigger = hlk_fm22x_ns.class_(
|
||||
"FaceScanMatchedTrigger", automation.Trigger.template(cg.int16, cg.std_string)
|
||||
)
|
||||
|
||||
FaceScanUnmatchedTrigger = hlk_fm22x_ns.class_(
|
||||
"FaceScanUnmatchedTrigger", automation.Trigger.template()
|
||||
)
|
||||
|
||||
FaceScanInvalidTrigger = hlk_fm22x_ns.class_(
|
||||
"FaceScanInvalidTrigger", automation.Trigger.template(cg.uint8)
|
||||
)
|
||||
|
||||
FaceInfoTrigger = hlk_fm22x_ns.class_(
|
||||
"FaceInfoTrigger",
|
||||
automation.Trigger.template(
|
||||
cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16
|
||||
),
|
||||
)
|
||||
|
||||
EnrollmentDoneTrigger = hlk_fm22x_ns.class_(
|
||||
"EnrollmentDoneTrigger", automation.Trigger.template(cg.int16, cg.uint8)
|
||||
)
|
||||
|
||||
EnrollmentFailedTrigger = hlk_fm22x_ns.class_(
|
||||
"EnrollmentFailedTrigger", automation.Trigger.template(cg.uint8)
|
||||
)
|
||||
|
||||
EnrollmentAction = hlk_fm22x_ns.class_("EnrollmentAction", automation.Action)
|
||||
DeleteAction = hlk_fm22x_ns.class_("DeleteAction", automation.Action)
|
||||
DeleteAllAction = hlk_fm22x_ns.class_("DeleteAllAction", automation.Action)
|
||||
ScanAction = hlk_fm22x_ns.class_("ScanAction", automation.Action)
|
||||
ResetAction = hlk_fm22x_ns.class_("ResetAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HlkFm22xComponent),
|
||||
cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
FaceScanMatchedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_FACE_SCAN_UNMATCHED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
FaceScanUnmatchedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
FaceScanInvalidTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FaceInfoTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
EnrollmentDoneTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
EnrollmentFailedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("50ms"))
|
||||
.extend(uart.UART_DEVICE_SCHEMA),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
for conf in config.get(CONF_ON_FACE_SCAN_MATCHED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.int16, "face_id"), (cg.std_string, "name")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_FACE_SCAN_UNMATCHED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_FACE_SCAN_INVALID, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.uint8, "error")], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_FACE_INFO, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger,
|
||||
[
|
||||
(cg.int16, "status"),
|
||||
(cg.int16, "left"),
|
||||
(cg.int16, "top"),
|
||||
(cg.int16, "right"),
|
||||
(cg.int16, "bottom"),
|
||||
(cg.int16, "yaw"),
|
||||
(cg.int16, "pitch"),
|
||||
(cg.int16, "roll"),
|
||||
],
|
||||
conf,
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_DONE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.int16, "face_id"), (cg.uint8, "direction")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.uint8, "error")], conf)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"hlk_fm22x.enroll",
|
||||
EnrollmentAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
|
||||
cv.Required(CONF_NAME): cv.templatable(cv.string),
|
||||
cv.Required(CONF_DIRECTION): cv.templatable(cv.uint8_t),
|
||||
},
|
||||
key=CONF_NAME,
|
||||
),
|
||||
)
|
||||
async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
template_ = await cg.templatable(config[CONF_NAME], args, cg.std_string)
|
||||
cg.add(var.set_name(template_))
|
||||
template_ = await cg.templatable(config[CONF_DIRECTION], args, cg.uint8)
|
||||
cg.add(var.set_direction(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"hlk_fm22x.delete",
|
||||
DeleteAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
|
||||
cv.Required(CONF_FACE_ID): cv.templatable(cv.uint16_t),
|
||||
},
|
||||
key=CONF_FACE_ID,
|
||||
),
|
||||
)
|
||||
async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
template_ = await cg.templatable(config[CONF_FACE_ID], args, cg.int16)
|
||||
cg.add(var.set_face_id(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"hlk_fm22x.delete_all",
|
||||
DeleteAllAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"hlk_fm22x.scan",
|
||||
ScanAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"hlk_fm22x.reset",
|
||||
ResetAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def hlk_fm22x_reset_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
21
esphome/components/hlk_fm22x/binary_sensor.py
Normal file
21
esphome/components/hlk_fm22x/binary_sensor.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ICON, ICON_KEY_PLUS
|
||||
|
||||
from . import CONF_HLK_FM22X_ID, HlkFm22xComponent
|
||||
|
||||
DEPENDENCIES = ["hlk_fm22x"]
|
||||
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
|
||||
{
|
||||
cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent),
|
||||
cv.Optional(CONF_ICON, default=ICON_KEY_PLUS): cv.icon,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_HLK_FM22X_ID])
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
cg.add(hub.set_enrolling_binary_sensor(var))
|
||||
325
esphome/components/hlk_fm22x/hlk_fm22x.cpp
Normal file
325
esphome/components/hlk_fm22x/hlk_fm22x.cpp
Normal file
@@ -0,0 +1,325 @@
|
||||
#include "hlk_fm22x.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::hlk_fm22x {
|
||||
|
||||
static const char *const TAG = "hlk_fm22x";
|
||||
|
||||
void HlkFm22xComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
|
||||
this->set_enrolling_(false);
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::update() {
|
||||
if (this->active_command_ != HlkFm22xCommand::NONE) {
|
||||
if (this->wait_cycles_ > 600) {
|
||||
ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
|
||||
if (HlkFm22xCommand::RESET == this->active_command_) {
|
||||
this->mark_failed();
|
||||
} else {
|
||||
this->reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
this->recv_command_();
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) {
|
||||
if (name.length() > 31) {
|
||||
ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "Starting enrollment for %s", name.c_str());
|
||||
std::array<uint8_t, 35> data{};
|
||||
data[0] = 0; // admin
|
||||
std::copy(name.begin(), name.end(), data.begin() + 1);
|
||||
// Remaining bytes are already zero-initialized
|
||||
data[33] = (uint8_t) direction;
|
||||
data[34] = 10; // timeout
|
||||
this->send_command_(HlkFm22xCommand::ENROLL, data.data(), data.size());
|
||||
this->set_enrolling_(true);
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::scan_face() {
|
||||
ESP_LOGI(TAG, "Verify face");
|
||||
static const uint8_t DATA[] = {0, 0};
|
||||
this->send_command_(HlkFm22xCommand::VERIFY, DATA, sizeof(DATA));
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::delete_face(int16_t face_id) {
|
||||
ESP_LOGI(TAG, "Deleting face in slot %d", face_id);
|
||||
const uint8_t data[] = {(uint8_t) (face_id >> 8), (uint8_t) (face_id & 0xFF)};
|
||||
this->send_command_(HlkFm22xCommand::DELETE_FACE, data, sizeof(data));
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::delete_all_faces() {
|
||||
ESP_LOGI(TAG, "Deleting all stored faces");
|
||||
this->send_command_(HlkFm22xCommand::DELETE_ALL_FACES);
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::get_face_count_() {
|
||||
ESP_LOGD(TAG, "Getting face count");
|
||||
this->send_command_(HlkFm22xCommand::GET_ALL_FACE_IDS);
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::reset() {
|
||||
ESP_LOGI(TAG, "Resetting module");
|
||||
this->active_command_ = HlkFm22xCommand::NONE;
|
||||
this->wait_cycles_ = 0;
|
||||
this->set_enrolling_(false);
|
||||
this->send_command_(HlkFm22xCommand::RESET);
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *data, size_t size) {
|
||||
ESP_LOGV(TAG, "Send command: 0x%.2X", command);
|
||||
if (this->active_command_ != HlkFm22xCommand::NONE) {
|
||||
ESP_LOGW(TAG, "Command 0x%.2X already active", this->active_command_);
|
||||
return;
|
||||
}
|
||||
this->wait_cycles_ = 0;
|
||||
this->active_command_ = command;
|
||||
while (this->available())
|
||||
this->read();
|
||||
this->write((uint8_t) (START_CODE >> 8));
|
||||
this->write((uint8_t) (START_CODE & 0xFF));
|
||||
this->write((uint8_t) command);
|
||||
uint16_t data_size = size;
|
||||
this->write((uint8_t) (data_size >> 8));
|
||||
this->write((uint8_t) (data_size & 0xFF));
|
||||
|
||||
uint8_t checksum = 0;
|
||||
checksum ^= (uint8_t) command;
|
||||
checksum ^= (data_size >> 8);
|
||||
checksum ^= (data_size & 0xFF);
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
this->write(data[i]);
|
||||
checksum ^= data[i];
|
||||
}
|
||||
|
||||
this->write(checksum);
|
||||
this->active_command_ = command;
|
||||
this->wait_cycles_ = 0;
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::recv_command_() {
|
||||
uint8_t byte, checksum = 0;
|
||||
uint16_t length = 0;
|
||||
|
||||
if (this->available() < 7) {
|
||||
++this->wait_cycles_;
|
||||
return;
|
||||
}
|
||||
this->wait_cycles_ = 0;
|
||||
|
||||
if ((this->read() != (uint8_t) (START_CODE >> 8)) || (this->read() != (uint8_t) (START_CODE & 0xFF))) {
|
||||
ESP_LOGE(TAG, "Invalid start code");
|
||||
return;
|
||||
}
|
||||
|
||||
byte = this->read();
|
||||
checksum ^= byte;
|
||||
HlkFm22xResponseType response_type = (HlkFm22xResponseType) byte;
|
||||
|
||||
byte = this->read();
|
||||
checksum ^= byte;
|
||||
length = byte << 8;
|
||||
byte = this->read();
|
||||
checksum ^= byte;
|
||||
length |= byte;
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
data.reserve(length);
|
||||
for (uint16_t idx = 0; idx < length; ++idx) {
|
||||
byte = this->read();
|
||||
checksum ^= byte;
|
||||
data.push_back(byte);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty(data).c_str());
|
||||
|
||||
byte = this->read();
|
||||
if (byte != checksum) {
|
||||
ESP_LOGE(TAG, "Invalid checksum for data. Calculated: 0x%.2X, Received: 0x%.2X", checksum, byte);
|
||||
return;
|
||||
}
|
||||
switch (response_type) {
|
||||
case HlkFm22xResponseType::NOTE:
|
||||
this->handle_note_(data);
|
||||
break;
|
||||
case HlkFm22xResponseType::REPLY:
|
||||
this->handle_reply_(data);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
|
||||
switch (data[0]) {
|
||||
case HlkFm22xNoteType::FACE_STATE:
|
||||
if (data.size() < 17) {
|
||||
ESP_LOGE(TAG, "Invalid face note data size: %u", data.size());
|
||||
break;
|
||||
}
|
||||
{
|
||||
int16_t info[8];
|
||||
uint8_t offset = 1;
|
||||
for (int16_t &i : info) {
|
||||
i = ((int16_t) data[offset + 1] << 8) | data[offset];
|
||||
offset += 2;
|
||||
}
|
||||
ESP_LOGV(TAG, "Face state: status: %d, left: %d, top: %d, right: %d, bottom: %d, yaw: %d, pitch: %d, roll: %d",
|
||||
info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
|
||||
this->face_info_callback_.call(info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
|
||||
}
|
||||
break;
|
||||
case HlkFm22xNoteType::READY:
|
||||
ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
|
||||
switch (this->active_command_) {
|
||||
case HlkFm22xCommand::ENROLL:
|
||||
this->set_enrolling_(false);
|
||||
this->enrollment_failed_callback_.call(HlkFm22xResult::FAILED4_TIMEOUT);
|
||||
break;
|
||||
case HlkFm22xCommand::VERIFY:
|
||||
this->face_scan_invalid_callback_.call(HlkFm22xResult::FAILED4_TIMEOUT);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this->active_command_ = HlkFm22xCommand::NONE;
|
||||
this->wait_cycles_ = 0;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unhandled note: 0x%.2X", data[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
|
||||
auto expected = this->active_command_;
|
||||
this->active_command_ = HlkFm22xCommand::NONE;
|
||||
if (data[0] != (uint8_t) expected) {
|
||||
ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data[1] != HlkFm22xResult::SUCCESS) {
|
||||
ESP_LOGE(TAG, "Command <0x%.2X> failed. Error: 0x%.2X", data[0], data[1]);
|
||||
switch (expected) {
|
||||
case HlkFm22xCommand::ENROLL:
|
||||
this->set_enrolling_(false);
|
||||
this->enrollment_failed_callback_.call(data[1]);
|
||||
break;
|
||||
case HlkFm22xCommand::VERIFY:
|
||||
if (data[1] == HlkFm22xResult::REJECTED) {
|
||||
this->face_scan_unmatched_callback_.call();
|
||||
} else {
|
||||
this->face_scan_invalid_callback_.call(data[1]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (expected) {
|
||||
case HlkFm22xCommand::VERIFY: {
|
||||
int16_t face_id = ((int16_t) data[2] << 8) | data[3];
|
||||
std::string name(data.begin() + 4, data.begin() + 36);
|
||||
ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str());
|
||||
if (this->last_face_id_sensor_ != nullptr) {
|
||||
this->last_face_id_sensor_->publish_state(face_id);
|
||||
}
|
||||
if (this->last_face_name_text_sensor_ != nullptr) {
|
||||
this->last_face_name_text_sensor_->publish_state(name);
|
||||
}
|
||||
this->face_scan_matched_callback_.call(face_id, name);
|
||||
break;
|
||||
}
|
||||
case HlkFm22xCommand::ENROLL: {
|
||||
int16_t face_id = ((int16_t) data[2] << 8) | data[3];
|
||||
HlkFm22xFaceDirection direction = (HlkFm22xFaceDirection) data[4];
|
||||
ESP_LOGI(TAG, "Face enrolled. ID: %d, Direction: 0x%.2X", face_id, direction);
|
||||
this->enrollment_done_callback_.call(face_id, (uint8_t) direction);
|
||||
this->set_enrolling_(false);
|
||||
this->defer([this]() { this->get_face_count_(); });
|
||||
break;
|
||||
}
|
||||
case HlkFm22xCommand::GET_STATUS:
|
||||
if (this->status_sensor_ != nullptr) {
|
||||
this->status_sensor_->publish_state(data[2]);
|
||||
}
|
||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
|
||||
break;
|
||||
case HlkFm22xCommand::GET_VERSION:
|
||||
if (this->version_text_sensor_ != nullptr) {
|
||||
std::string version(data.begin() + 2, data.end());
|
||||
this->version_text_sensor_->publish_state(version);
|
||||
}
|
||||
this->defer([this]() { this->get_face_count_(); });
|
||||
break;
|
||||
case HlkFm22xCommand::GET_ALL_FACE_IDS:
|
||||
if (this->face_count_sensor_ != nullptr) {
|
||||
this->face_count_sensor_->publish_state(data[2]);
|
||||
}
|
||||
break;
|
||||
case HlkFm22xCommand::DELETE_FACE:
|
||||
ESP_LOGI(TAG, "Deleted face");
|
||||
break;
|
||||
case HlkFm22xCommand::DELETE_ALL_FACES:
|
||||
ESP_LOGI(TAG, "Deleted all faces");
|
||||
break;
|
||||
case HlkFm22xCommand::RESET:
|
||||
ESP_LOGI(TAG, "Module reset");
|
||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unhandled command: 0x%.2X", this->active_command_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::set_enrolling_(bool enrolling) {
|
||||
if (this->enrolling_binary_sensor_ != nullptr) {
|
||||
this->enrolling_binary_sensor_->publish_state(enrolling);
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "HLK_FM22X:");
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
if (this->version_text_sensor_) {
|
||||
LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %s", this->version_text_sensor_->get_state().c_str());
|
||||
}
|
||||
if (this->enrolling_binary_sensor_) {
|
||||
LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF");
|
||||
}
|
||||
if (this->face_count_sensor_) {
|
||||
LOG_SENSOR(" ", "Face Count", this->face_count_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %u", (uint16_t) this->face_count_sensor_->get_state());
|
||||
}
|
||||
if (this->status_sensor_) {
|
||||
LOG_SENSOR(" ", "Status", this->status_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %u", (uint8_t) this->status_sensor_->get_state());
|
||||
}
|
||||
if (this->last_face_id_sensor_) {
|
||||
LOG_SENSOR(" ", "Last Face ID", this->last_face_id_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %u", (int16_t) this->last_face_id_sensor_->get_state());
|
||||
}
|
||||
if (this->last_face_name_text_sensor_) {
|
||||
LOG_TEXT_SENSOR(" ", "Last Face Name", this->last_face_name_text_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %s", this->last_face_name_text_sensor_->get_state().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::hlk_fm22x
|
||||
224
esphome/components/hlk_fm22x/hlk_fm22x.h
Normal file
224
esphome/components/hlk_fm22x/hlk_fm22x.h
Normal file
@@ -0,0 +1,224 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::hlk_fm22x {
|
||||
|
||||
static const uint16_t START_CODE = 0xEFAA;
|
||||
enum HlkFm22xCommand {
|
||||
NONE = 0x00,
|
||||
RESET = 0x10,
|
||||
GET_STATUS = 0x11,
|
||||
VERIFY = 0x12,
|
||||
ENROLL = 0x13,
|
||||
DELETE_FACE = 0x20,
|
||||
DELETE_ALL_FACES = 0x21,
|
||||
GET_ALL_FACE_IDS = 0x24,
|
||||
GET_VERSION = 0x30,
|
||||
GET_SERIAL_NUMBER = 0x93,
|
||||
};
|
||||
|
||||
enum HlkFm22xResponseType {
|
||||
REPLY = 0x00,
|
||||
NOTE = 0x01,
|
||||
IMAGE = 0x02,
|
||||
};
|
||||
|
||||
enum HlkFm22xNoteType {
|
||||
READY = 0x00,
|
||||
FACE_STATE = 0x01,
|
||||
};
|
||||
|
||||
enum HlkFm22xResult {
|
||||
SUCCESS = 0x00,
|
||||
REJECTED = 0x01,
|
||||
ABORTED = 0x02,
|
||||
FAILED4_CAMERA = 0x04,
|
||||
FAILED4_UNKNOWNREASON = 0x05,
|
||||
FAILED4_INVALIDPARAM = 0x06,
|
||||
FAILED4_NOMEMORY = 0x07,
|
||||
FAILED4_UNKNOWNUSER = 0x08,
|
||||
FAILED4_MAXUSER = 0x09,
|
||||
FAILED4_FACEENROLLED = 0x0A,
|
||||
FAILED4_LIVENESSCHECK = 0x0C,
|
||||
FAILED4_TIMEOUT = 0x0D,
|
||||
FAILED4_AUTHORIZATION = 0x0E,
|
||||
FAILED4_READ_FILE = 0x13,
|
||||
FAILED4_WRITE_FILE = 0x14,
|
||||
FAILED4_NO_ENCRYPT = 0x15,
|
||||
FAILED4_NO_RGBIMAGE = 0x17,
|
||||
FAILED4_JPGPHOTO_LARGE = 0x18,
|
||||
FAILED4_JPGPHOTO_SMALL = 0x19,
|
||||
};
|
||||
|
||||
enum HlkFm22xFaceDirection {
|
||||
FACE_DIRECTION_UNDEFINED = 0x00,
|
||||
FACE_DIRECTION_MIDDLE = 0x01,
|
||||
FACE_DIRECTION_RIGHT = 0x02,
|
||||
FACE_DIRECTION_LEFT = 0x04,
|
||||
FACE_DIRECTION_DOWN = 0x08,
|
||||
FACE_DIRECTION_UP = 0x10,
|
||||
};
|
||||
|
||||
class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_face_count_sensor(sensor::Sensor *face_count_sensor) { this->face_count_sensor_ = face_count_sensor; }
|
||||
void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; }
|
||||
void set_last_face_id_sensor(sensor::Sensor *last_face_id_sensor) {
|
||||
this->last_face_id_sensor_ = last_face_id_sensor;
|
||||
}
|
||||
void set_last_face_name_text_sensor(text_sensor::TextSensor *last_face_name_text_sensor) {
|
||||
this->last_face_name_text_sensor_ = last_face_name_text_sensor;
|
||||
}
|
||||
void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) {
|
||||
this->enrolling_binary_sensor_ = enrolling_binary_sensor;
|
||||
}
|
||||
void set_version_text_sensor(text_sensor::TextSensor *version_text_sensor) {
|
||||
this->version_text_sensor_ = version_text_sensor;
|
||||
}
|
||||
void add_on_face_scan_matched_callback(std::function<void(int16_t, std::string)> callback) {
|
||||
this->face_scan_matched_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_face_scan_unmatched_callback(std::function<void()> callback) {
|
||||
this->face_scan_unmatched_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_face_scan_invalid_callback(std::function<void(uint8_t)> callback) {
|
||||
this->face_scan_invalid_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_face_info_callback(
|
||||
std::function<void(int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t)> callback) {
|
||||
this->face_info_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_enrollment_done_callback(std::function<void(int16_t, uint8_t)> callback) {
|
||||
this->enrollment_done_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_enrollment_failed_callback(std::function<void(uint8_t)> callback) {
|
||||
this->enrollment_failed_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void enroll_face(const std::string &name, HlkFm22xFaceDirection direction);
|
||||
void scan_face();
|
||||
void delete_face(int16_t face_id);
|
||||
void delete_all_faces();
|
||||
void reset();
|
||||
|
||||
protected:
|
||||
void get_face_count_();
|
||||
void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0);
|
||||
void recv_command_();
|
||||
void handle_note_(const std::vector<uint8_t> &data);
|
||||
void handle_reply_(const std::vector<uint8_t> &data);
|
||||
void set_enrolling_(bool enrolling);
|
||||
|
||||
HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE;
|
||||
uint16_t wait_cycles_ = 0;
|
||||
sensor::Sensor *face_count_sensor_{nullptr};
|
||||
sensor::Sensor *status_sensor_{nullptr};
|
||||
sensor::Sensor *last_face_id_sensor_{nullptr};
|
||||
binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr};
|
||||
text_sensor::TextSensor *last_face_name_text_sensor_{nullptr};
|
||||
text_sensor::TextSensor *version_text_sensor_{nullptr};
|
||||
CallbackManager<void(uint8_t)> face_scan_invalid_callback_;
|
||||
CallbackManager<void(int16_t, std::string)> face_scan_matched_callback_;
|
||||
CallbackManager<void()> face_scan_unmatched_callback_;
|
||||
CallbackManager<void(int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t)> face_info_callback_;
|
||||
CallbackManager<void(int16_t, uint8_t)> enrollment_done_callback_;
|
||||
CallbackManager<void(uint8_t)> enrollment_failed_callback_;
|
||||
};
|
||||
|
||||
class FaceScanMatchedTrigger : public Trigger<int16_t, std::string> {
|
||||
public:
|
||||
explicit FaceScanMatchedTrigger(HlkFm22xComponent *parent) {
|
||||
parent->add_on_face_scan_matched_callback(
|
||||
[this](int16_t face_id, const std::string &name) { this->trigger(face_id, name); });
|
||||
}
|
||||
};
|
||||
|
||||
class FaceScanUnmatchedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit FaceScanUnmatchedTrigger(HlkFm22xComponent *parent) {
|
||||
parent->add_on_face_scan_unmatched_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class FaceScanInvalidTrigger : public Trigger<uint8_t> {
|
||||
public:
|
||||
explicit FaceScanInvalidTrigger(HlkFm22xComponent *parent) {
|
||||
parent->add_on_face_scan_invalid_callback([this](uint8_t error) { this->trigger(error); });
|
||||
}
|
||||
};
|
||||
|
||||
class FaceInfoTrigger : public Trigger<int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t> {
|
||||
public:
|
||||
explicit FaceInfoTrigger(HlkFm22xComponent *parent) {
|
||||
parent->add_on_face_info_callback(
|
||||
[this](int16_t status, int16_t left, int16_t top, int16_t right, int16_t bottom, int16_t yaw, int16_t pitch,
|
||||
int16_t roll) { this->trigger(status, left, top, right, bottom, yaw, pitch, roll); });
|
||||
}
|
||||
};
|
||||
|
||||
class EnrollmentDoneTrigger : public Trigger<int16_t, uint8_t> {
|
||||
public:
|
||||
explicit EnrollmentDoneTrigger(HlkFm22xComponent *parent) {
|
||||
parent->add_on_enrollment_done_callback(
|
||||
[this](int16_t face_id, uint8_t direction) { this->trigger(face_id, direction); });
|
||||
}
|
||||
};
|
||||
|
||||
class EnrollmentFailedTrigger : public Trigger<uint8_t> {
|
||||
public:
|
||||
explicit EnrollmentFailedTrigger(HlkFm22xComponent *parent) {
|
||||
parent->add_on_enrollment_failed_callback([this](uint8_t error) { this->trigger(error); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class EnrollmentAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(std::string, name)
|
||||
TEMPLATABLE_VALUE(uint8_t, direction)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto name = this->name_.value(x...);
|
||||
auto direction = (HlkFm22xFaceDirection) this->direction_.value(x...);
|
||||
this->parent_->enroll_face(name, direction);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class DeleteAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(int16_t, face_id)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto face_id = this->face_id_.value(x...);
|
||||
this->parent_->delete_face(face_id);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class DeleteAllAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->delete_all_faces(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ScanAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->scan_face(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ResetAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->reset(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::hlk_fm22x
|
||||
47
esphome/components/hlk_fm22x/sensor.py
Normal file
47
esphome/components/hlk_fm22x/sensor.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_STATUS, ENTITY_CATEGORY_DIAGNOSTIC, ICON_ACCOUNT
|
||||
|
||||
from . import CONF_HLK_FM22X_ID, HlkFm22xComponent
|
||||
|
||||
DEPENDENCIES = ["hlk_fm22x"]
|
||||
|
||||
CONF_FACE_COUNT = "face_count"
|
||||
CONF_LAST_FACE_ID = "last_face_id"
|
||||
ICON_FACE = "mdi:face-recognition"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent),
|
||||
cv.Optional(CONF_FACE_COUNT): sensor.sensor_schema(
|
||||
icon=ICON_FACE,
|
||||
accuracy_decimals=0,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_STATUS): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_LAST_FACE_ID): sensor.sensor_schema(
|
||||
icon=ICON_ACCOUNT,
|
||||
accuracy_decimals=0,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_HLK_FM22X_ID])
|
||||
|
||||
for key in [
|
||||
CONF_FACE_COUNT,
|
||||
CONF_STATUS,
|
||||
CONF_LAST_FACE_ID,
|
||||
]:
|
||||
if key not in config:
|
||||
continue
|
||||
conf = config[key]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(getattr(hub, f"set_{key}_sensor")(sens))
|
||||
42
esphome/components/hlk_fm22x/text_sensor.py
Normal file
42
esphome/components/hlk_fm22x/text_sensor.py
Normal file
@@ -0,0 +1,42 @@
|
||||
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_ACCOUNT,
|
||||
ICON_RESTART,
|
||||
)
|
||||
|
||||
from . import CONF_HLK_FM22X_ID, HlkFm22xComponent
|
||||
|
||||
DEPENDENCIES = ["hlk_fm22x"]
|
||||
|
||||
CONF_LAST_FACE_NAME = "last_face_name"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent),
|
||||
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
|
||||
icon=ICON_RESTART,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_LAST_FACE_NAME): text_sensor.text_sensor_schema(
|
||||
icon=ICON_ACCOUNT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_HLK_FM22X_ID])
|
||||
for key in [
|
||||
CONF_VERSION,
|
||||
CONF_LAST_FACE_NAME,
|
||||
]:
|
||||
if key not in config:
|
||||
continue
|
||||
conf = config[key]
|
||||
sens = await text_sensor.new_text_sensor(conf)
|
||||
cg.add(getattr(hub, f"set_{key}_text_sensor")(sens))
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <climits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
// https://www.airnow.gov/sites/default/files/2020-05/aqi-technical-assistance-document-sept2018.pdf
|
||||
// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
|
||||
|
||||
namespace esphome {
|
||||
namespace hm3301 {
|
||||
@@ -16,16 +16,15 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
}
|
||||
|
||||
protected:
|
||||
static const int AMOUNT_OF_LEVELS = 7;
|
||||
static const int AMOUNT_OF_LEVELS = 6;
|
||||
|
||||
int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200},
|
||||
{201, 300}, {301, 400}, {401, 500}};
|
||||
int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
|
||||
|
||||
int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 35}, {36, 55}, {56, 150},
|
||||
{151, 250}, {251, 350}, {351, 500}};
|
||||
int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55},
|
||||
{56, 125}, {126, 225}, {226, INT_MAX}};
|
||||
|
||||
int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, {255, 354},
|
||||
{355, 424}, {425, 504}, {505, 604}};
|
||||
int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254},
|
||||
{255, 354}, {355, 424}, {425, INT_MAX}};
|
||||
|
||||
int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) {
|
||||
int grid_index = get_grid_index_(value, array);
|
||||
|
||||
@@ -7,10 +7,8 @@ namespace homeassistant {
|
||||
static const char *const TAG = "homeassistant.time";
|
||||
|
||||
void HomeassistantTime::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Home Assistant Time:\n"
|
||||
" Timezone: '%s'",
|
||||
this->timezone_.c_str());
|
||||
ESP_LOGCONFIG(TAG, "Home Assistant Time");
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
@@ -231,7 +231,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
this->connecting_sta_ = sta;
|
||||
|
||||
wifi::global_wifi_component->set_sta(sta);
|
||||
wifi::global_wifi_component->start_connecting(sta, false);
|
||||
wifi::global_wifi_component->start_connecting(sta);
|
||||
this->set_state_(improv::STATE_PROVISIONING);
|
||||
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||
command.password.c_str());
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import importlib
|
||||
import logging
|
||||
import pkgutil
|
||||
|
||||
from esphome.automation import build_automation, register_action, validate_automation
|
||||
from esphome.automation import build_automation, validate_automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING
|
||||
from esphome.components.display import Display
|
||||
@@ -25,8 +27,8 @@ from esphome.cpp_generator import MockObj
|
||||
from esphome.final_validate import full_config
|
||||
from esphome.helpers import write_file_if_changed
|
||||
|
||||
from . import defines as df, helpers, lv_validation as lvalid
|
||||
from .automation import disp_update, focused_widgets, refreshed_widgets, update_to_code
|
||||
from . import defines as df, helpers, lv_validation as lvalid, widgets
|
||||
from .automation import disp_update, focused_widgets, refreshed_widgets
|
||||
from .defines import add_define
|
||||
from .encoders import (
|
||||
ENCODERS_CONFIG,
|
||||
@@ -45,7 +47,6 @@ from .schemas import (
|
||||
WIDGET_TYPES,
|
||||
any_widget_schema,
|
||||
container_schema,
|
||||
create_modify_schema,
|
||||
obj_schema,
|
||||
)
|
||||
from .styles import add_top_layer, styles_to_code, theme_to_code
|
||||
@@ -54,7 +55,6 @@ from .trigger import add_on_boot_triggers, generate_triggers
|
||||
from .types import (
|
||||
FontEngine,
|
||||
IdleTrigger,
|
||||
ObjUpdateAction,
|
||||
PlainTrigger,
|
||||
lv_font_t,
|
||||
lv_group_t,
|
||||
@@ -69,33 +69,23 @@ from .widgets import (
|
||||
set_obj_properties,
|
||||
styles_used,
|
||||
)
|
||||
from .widgets.animimg import animimg_spec
|
||||
from .widgets.arc import arc_spec
|
||||
from .widgets.button import button_spec
|
||||
from .widgets.buttonmatrix import buttonmatrix_spec
|
||||
from .widgets.canvas import canvas_spec
|
||||
from .widgets.checkbox import checkbox_spec
|
||||
from .widgets.container import container_spec
|
||||
from .widgets.dropdown import dropdown_spec
|
||||
from .widgets.img import img_spec
|
||||
from .widgets.keyboard import keyboard_spec
|
||||
from .widgets.label import label_spec
|
||||
from .widgets.led import led_spec
|
||||
from .widgets.line import line_spec
|
||||
from .widgets.lv_bar import bar_spec
|
||||
from .widgets.meter import meter_spec
|
||||
|
||||
# Import only what we actually use directly in this file
|
||||
from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
|
||||
from .widgets.obj import obj_spec
|
||||
from .widgets.page import add_pages, generate_page_triggers, page_spec
|
||||
from .widgets.qrcode import qr_code_spec
|
||||
from .widgets.roller import roller_spec
|
||||
from .widgets.slider import slider_spec
|
||||
from .widgets.spinbox import spinbox_spec
|
||||
from .widgets.spinner import spinner_spec
|
||||
from .widgets.switch import switch_spec
|
||||
from .widgets.tabview import tabview_spec
|
||||
from .widgets.textarea import textarea_spec
|
||||
from .widgets.tileview import tileview_spec
|
||||
from .widgets.obj import obj_spec # Used in LVGL_SCHEMA
|
||||
from .widgets.page import ( # page_spec used in LVGL_SCHEMA
|
||||
add_pages,
|
||||
generate_page_triggers,
|
||||
page_spec,
|
||||
)
|
||||
|
||||
# Widget registration happens via WidgetType.__init__ in individual widget files
|
||||
# The imports below trigger creation of the widget types
|
||||
# Action registration (lvgl.{widget}.update) happens automatically
|
||||
# in the WidgetType.__init__ method
|
||||
|
||||
for module_info in pkgutil.iter_modules(widgets.__path__):
|
||||
importlib.import_module(f".widgets.{module_info.name}", package=__package__)
|
||||
|
||||
DOMAIN = "lvgl"
|
||||
DEPENDENCIES = ["display"]
|
||||
@@ -103,41 +93,6 @@ AUTO_LOAD = ["key_provider"]
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
for w_type in (
|
||||
label_spec,
|
||||
obj_spec,
|
||||
button_spec,
|
||||
bar_spec,
|
||||
slider_spec,
|
||||
arc_spec,
|
||||
line_spec,
|
||||
spinner_spec,
|
||||
led_spec,
|
||||
animimg_spec,
|
||||
checkbox_spec,
|
||||
img_spec,
|
||||
switch_spec,
|
||||
tabview_spec,
|
||||
buttonmatrix_spec,
|
||||
meter_spec,
|
||||
dropdown_spec,
|
||||
roller_spec,
|
||||
textarea_spec,
|
||||
spinbox_spec,
|
||||
keyboard_spec,
|
||||
tileview_spec,
|
||||
qr_code_spec,
|
||||
canvas_spec,
|
||||
container_spec,
|
||||
):
|
||||
WIDGET_TYPES[w_type.name] = w_type
|
||||
|
||||
for w_type in WIDGET_TYPES.values():
|
||||
register_action(
|
||||
f"lvgl.{w_type.name}.update",
|
||||
ObjUpdateAction,
|
||||
create_modify_schema(w_type),
|
||||
)(update_to_code)
|
||||
|
||||
SIMPLE_TRIGGERS = (
|
||||
df.CONF_ON_PAUSE,
|
||||
@@ -376,7 +331,7 @@ async def to_code(configs):
|
||||
# This must be done after all widgets are created
|
||||
for comp in helpers.lvgl_components_required:
|
||||
cg.add_define(f"USE_LVGL_{comp.upper()}")
|
||||
if "transform_angle" in styles_used:
|
||||
if {"transform_angle", "transform_zoom"} & styles_used:
|
||||
add_define("LV_COLOR_SCREEN_TRANSP", "1")
|
||||
for use in helpers.lv_uses:
|
||||
add_define(f"LV_USE_{use.upper()}")
|
||||
@@ -402,6 +357,15 @@ def add_hello_world(config):
|
||||
return config
|
||||
|
||||
|
||||
def _theme_schema(value):
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA)
|
||||
for name, w in WIDGET_TYPES.items()
|
||||
}
|
||||
)(value)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validation
|
||||
|
||||
LVGL_SCHEMA = cv.All(
|
||||
@@ -454,12 +418,7 @@ LVGL_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
df.CONF_TRANSPARENCY_KEY, default=0x000400
|
||||
): lvalid.lv_color,
|
||||
cv.Optional(df.CONF_THEME): cv.Schema(
|
||||
{
|
||||
cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA)
|
||||
for name, w in WIDGET_TYPES.items()
|
||||
}
|
||||
),
|
||||
cv.Optional(df.CONF_THEME): _theme_schema,
|
||||
cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA,
|
||||
cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema,
|
||||
cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG,
|
||||
|
||||
@@ -411,6 +411,10 @@ def any_widget_schema(extras=None):
|
||||
Dynamically generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of
|
||||
widget under the widgets: key.
|
||||
|
||||
This uses lazy evaluation - the schema is built when called during validation,
|
||||
not at import time. This allows external components to register widgets
|
||||
before schema validation begins.
|
||||
|
||||
:param extras: Additional schema to be applied to each generated one
|
||||
:return: A validator for the Widgets key
|
||||
"""
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import sys
|
||||
|
||||
from esphome import automation, codegen as cg
|
||||
from esphome.automation import register_action
|
||||
from esphome.config_validation import Schema
|
||||
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.cpp_types import esphome_ns
|
||||
|
||||
@@ -124,13 +126,16 @@ class WidgetType:
|
||||
schema=None,
|
||||
modify_schema=None,
|
||||
lv_name=None,
|
||||
is_mock: bool = False,
|
||||
):
|
||||
"""
|
||||
:param name: The widget name, e.g. "bar"
|
||||
:param w_type: The C type of the widget
|
||||
:param parts: What parts this widget supports
|
||||
:param schema: The config schema for defining a widget
|
||||
:param modify_schema: A schema to update the widget
|
||||
:param modify_schema: A schema to update the widget, defaults to the same as the schema
|
||||
:param lv_name: The name of the LVGL widget in the LVGL library, if different from the name
|
||||
:param is_mock: Whether this widget is a mock widget, i.e. not a real LVGL widget
|
||||
"""
|
||||
self.name = name
|
||||
self.lv_name = lv_name or name
|
||||
@@ -146,6 +151,22 @@ class WidgetType:
|
||||
self.modify_schema = modify_schema
|
||||
self.mock_obj = MockObj(f"lv_{self.lv_name}", "_")
|
||||
|
||||
# Local import to avoid circular import
|
||||
from .automation import update_to_code
|
||||
from .schemas import WIDGET_TYPES, create_modify_schema
|
||||
|
||||
if not is_mock:
|
||||
if self.name in WIDGET_TYPES:
|
||||
raise EsphomeError(f"Duplicate definition of widget type '{self.name}'")
|
||||
WIDGET_TYPES[self.name] = self
|
||||
|
||||
# Register the update action automatically
|
||||
register_action(
|
||||
f"lvgl.{self.name}.update",
|
||||
ObjUpdateAction,
|
||||
create_modify_schema(self),
|
||||
)(update_to_code)
|
||||
|
||||
@property
|
||||
def animated(self):
|
||||
return False
|
||||
|
||||
@@ -213,17 +213,14 @@ class LvScrActType(WidgetType):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("lv_scr_act()", lv_obj_t, ())
|
||||
super().__init__("lv_scr_act()", lv_obj_t, (), is_mock=True)
|
||||
|
||||
async def to_code(self, w, config: dict):
|
||||
return []
|
||||
|
||||
|
||||
lv_scr_act_spec = LvScrActType()
|
||||
|
||||
|
||||
def get_scr_act(lv_comp: MockObj) -> Widget:
|
||||
return Widget.create(None, lv_comp.get_scr_act(), lv_scr_act_spec, {})
|
||||
return Widget.create(None, lv_comp.get_scr_act(), LvScrActType(), {})
|
||||
|
||||
|
||||
def get_widget_generator(wid):
|
||||
|
||||
@@ -2,7 +2,7 @@ from esphome import automation
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE
|
||||
|
||||
from ..automation import action_to_code, update_to_code
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
CONF_CURSOR,
|
||||
CONF_DECIMAL_PLACES,
|
||||
@@ -171,17 +171,3 @@ async def spinbox_decrement(config, action_id, template_arg, args):
|
||||
lv.spinbox_decrement(w.obj)
|
||||
|
||||
return await action_to_code(widgets, do_increment, action_id, template_arg, args)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.spinbox.update",
|
||||
ObjUpdateAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(lv_spinbox_t),
|
||||
cv.Required(CONF_VALUE): lv_float,
|
||||
}
|
||||
),
|
||||
)
|
||||
async def spinbox_update_to_code(config, action_id, template_arg, args):
|
||||
return await update_to_code(config, action_id, template_arg, args)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
@@ -9,6 +11,13 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
AUTO_LOAD = ["mdns"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# High performance networking tracking infrastructure
|
||||
# Components can request high performance networking and this configures lwip and WiFi settings
|
||||
KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking"
|
||||
CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance"
|
||||
|
||||
network_ns = cg.esphome_ns.namespace("network")
|
||||
IPAddress = network_ns.class_("IPAddress")
|
||||
|
||||
@@ -47,6 +56,55 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj:
|
||||
return IPAddress(str(ip))
|
||||
|
||||
|
||||
def require_high_performance_networking() -> None:
|
||||
"""Request high performance networking for network and WiFi.
|
||||
|
||||
Call this from components that need optimized network performance for streaming
|
||||
or high-throughput data transfer. This enables high performance mode which
|
||||
configures both lwip TCP settings and WiFi driver settings for improved
|
||||
network performance.
|
||||
|
||||
Settings applied (ESP-IDF only):
|
||||
- lwip: Larger TCP buffers, windows, and mailbox sizes
|
||||
- WiFi: Increased RX/TX buffers, AMPDU aggregation, PSRAM allocation (set by wifi component)
|
||||
|
||||
Configuration is PSRAM-aware:
|
||||
- With PSRAM guaranteed: Aggressive settings (512 RX buffers, 512KB TCP windows)
|
||||
- Without PSRAM: Conservative optimized settings (64 buffers, 65KB TCP windows)
|
||||
|
||||
Example:
|
||||
from esphome.components import network
|
||||
|
||||
def _request_high_performance_networking(config):
|
||||
network.require_high_performance_networking()
|
||||
return config
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
...,
|
||||
_request_high_performance_networking,
|
||||
)
|
||||
"""
|
||||
# Only set up once (idempotent - multiple components can call this)
|
||||
if not CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False):
|
||||
CORE.data[KEY_HIGH_PERFORMANCE_NETWORKING] = True
|
||||
|
||||
|
||||
def has_high_performance_networking() -> bool:
|
||||
"""Check if high performance networking mode is enabled.
|
||||
|
||||
Returns True when high performance networking has been requested by a
|
||||
component or explicitly enabled in the network configuration. This indicates
|
||||
that lwip and WiFi will use optimized buffer sizes and settings.
|
||||
|
||||
This function should be called during code generation (to_code phase) by
|
||||
components that need to apply performance-related settings.
|
||||
|
||||
Returns:
|
||||
bool: True if high performance networking is enabled, False otherwise
|
||||
"""
|
||||
return CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.SplitDefault(
|
||||
@@ -71,6 +129,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int,
|
||||
cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All(cv.boolean, cv.only_on_esp32),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -80,6 +139,70 @@ async def to_code(config):
|
||||
cg.add_define("USE_NETWORK")
|
||||
if CORE.using_arduino and CORE.is_esp32:
|
||||
cg.add_library("Networking", None)
|
||||
|
||||
# Apply high performance networking settings
|
||||
# Config can explicitly enable/disable, or default to component-driven behavior
|
||||
enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE)
|
||||
component_requested = CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False)
|
||||
|
||||
# Explicit config overrides component request
|
||||
should_enable = (
|
||||
enable_high_perf if enable_high_perf is not None else component_requested
|
||||
)
|
||||
|
||||
# Log when user explicitly disables but a component requested it
|
||||
if enable_high_perf is False and component_requested:
|
||||
_LOGGER.info(
|
||||
"High performance networking disabled by user configuration (overriding component request)"
|
||||
)
|
||||
|
||||
if CORE.is_esp32 and CORE.using_esp_idf and should_enable:
|
||||
# Check if PSRAM is guaranteed (set by psram component during final validation)
|
||||
psram_guaranteed = psram_is_guaranteed()
|
||||
|
||||
if psram_guaranteed:
|
||||
_LOGGER.info(
|
||||
"Applying high-performance lwip settings (PSRAM guaranteed): 512KB TCP windows, 512 mailbox sizes"
|
||||
)
|
||||
# PSRAM is guaranteed - use aggressive settings
|
||||
# Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true
|
||||
# CONFIG_LWIP_WND_SCALE can only be enabled if CONFIG_SPIRAM_IGNORE_NOTFOUND isn't set
|
||||
# Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702
|
||||
|
||||
# Enable window scaling for much larger TCP windows
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RCV_SCALE", 3)
|
||||
|
||||
# Large TCP buffers and windows (requires PSRAM)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 512000)
|
||||
|
||||
# Large mailboxes for high throughput
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 512)
|
||||
|
||||
# TCP connection limits
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16)
|
||||
|
||||
# TCP optimizations
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MAXRTX", 12)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SYNMAXRTX", 6)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSS", 1436)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSL", 60000)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_OVERSIZE_MSS", True)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_QUEUE_OOSEQ", True)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Applying optimized lwip settings: 65KB TCP windows, 64 mailbox sizes"
|
||||
)
|
||||
# PSRAM not guaranteed - use more conservative, but still optimized settings
|
||||
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64)
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64)
|
||||
|
||||
if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None:
|
||||
cg.add_define("USE_NETWORK_IPV6", enable_ipv6)
|
||||
if enable_ipv6:
|
||||
|
||||
@@ -25,6 +25,7 @@ from esphome.const import (
|
||||
CONF_FRAMEWORK,
|
||||
CONF_ID,
|
||||
CONF_RESET_PIN,
|
||||
CONF_VOLTAGE,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
@@ -102,6 +103,10 @@ nrf52_ns = cg.esphome_ns.namespace("nrf52")
|
||||
DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component)
|
||||
|
||||
CONF_DFU = "dfu"
|
||||
CONF_REG0 = "reg0"
|
||||
CONF_UICR_ERASE = "uicr_erase"
|
||||
|
||||
VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3]
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
_detect_bootloader,
|
||||
@@ -116,6 +121,15 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_REG0): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_VOLTAGE): cv.All(
|
||||
cv.voltage,
|
||||
cv.one_of(*VOLTAGE_LEVELS, float=True),
|
||||
),
|
||||
cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -183,6 +197,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
if dfu_config := config.get(CONF_DFU):
|
||||
CORE.add_job(_dfu_to_code, dfu_config)
|
||||
|
||||
if reg0_config := config.get(CONF_REG0):
|
||||
value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE])
|
||||
cg.add_define("USE_NRF52_REG0_VOUT", value)
|
||||
if reg0_config[CONF_UICR_ERASE]:
|
||||
cg.add_define("USE_NRF52_UICR_ERASE")
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.DIAGNOSTICS)
|
||||
async def _dfu_to_code(dfu_config):
|
||||
|
||||
121
esphome/components/nrf52/uicr.cpp
Normal file
121
esphome/components/nrf52/uicr.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_NRF52_REG0_VOUT
|
||||
#include <zephyr/init.h>
|
||||
#include <hal/nrf_power.h>
|
||||
#include <zephyr/sys/printk.h>
|
||||
|
||||
extern "C" {
|
||||
void nvmc_config(uint32_t mode);
|
||||
void nvmc_wait();
|
||||
nrfx_err_t nrfx_nvmc_uicr_erase();
|
||||
}
|
||||
|
||||
namespace esphome::nrf52 {
|
||||
|
||||
enum class StatusFlags : uint8_t {
|
||||
OK = 0x00,
|
||||
NEED_RESET = 0x01,
|
||||
NEED_ERASE = 0x02,
|
||||
};
|
||||
|
||||
constexpr StatusFlags &operator|=(StatusFlags &a, StatusFlags b) {
|
||||
a = static_cast<StatusFlags>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
|
||||
return a;
|
||||
}
|
||||
|
||||
constexpr bool operator&(StatusFlags a, StatusFlags b) {
|
||||
return (static_cast<uint8_t>(a) & static_cast<uint8_t>(b)) != 0;
|
||||
}
|
||||
|
||||
static bool regout0_ok() {
|
||||
return (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) == (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos);
|
||||
}
|
||||
|
||||
static StatusFlags set_regout0() {
|
||||
/* If the board is powered from USB (high voltage mode),
|
||||
* GPIO output voltage is set to 1.8 volts by default.
|
||||
*/
|
||||
if (!regout0_ok()) {
|
||||
nvmc_config(NVMC_CONFIG_WEN_Wen);
|
||||
NRF_UICR->REGOUT0 =
|
||||
(NRF_UICR->REGOUT0 & ~((uint32_t) UICR_REGOUT0_VOUT_Msk)) | (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos);
|
||||
nvmc_wait();
|
||||
nvmc_config(NVMC_CONFIG_WEN_Ren);
|
||||
return regout0_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE;
|
||||
}
|
||||
return StatusFlags::OK;
|
||||
}
|
||||
|
||||
#ifndef USE_BOOTLOADER_MCUBOOT
|
||||
// https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/6a9a6a3e6d0f86918e9286188426a279976645bd/lib/sdk11/components/libraries/bootloader_dfu/dfu_types.h#L61
|
||||
constexpr uint32_t BOOTLOADER_REGION_START = 0x000F4000;
|
||||
constexpr uint32_t BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS = 0x000FE000;
|
||||
|
||||
static bool bootloader_ok() {
|
||||
return NRF_UICR->NRFFW[0] == BOOTLOADER_REGION_START && NRF_UICR->NRFFW[1] == BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS;
|
||||
}
|
||||
|
||||
static StatusFlags fix_bootloader() {
|
||||
if (!bootloader_ok()) {
|
||||
nvmc_config(NVMC_CONFIG_WEN_Wen);
|
||||
NRF_UICR->NRFFW[0] = BOOTLOADER_REGION_START;
|
||||
NRF_UICR->NRFFW[1] = BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS;
|
||||
nvmc_wait();
|
||||
nvmc_config(NVMC_CONFIG_WEN_Ren);
|
||||
return bootloader_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE;
|
||||
}
|
||||
return StatusFlags::OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
|
||||
|
||||
static StatusFlags set_uicr() {
|
||||
StatusFlags status = StatusFlags::OK;
|
||||
#ifndef USE_BOOTLOADER_MCUBOOT
|
||||
if (BOOTLOADER_VERSION_REGISTER <= 0x902) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("cannot control regout0 for %#x\n", BOOTLOADER_VERSION_REGISTER);
|
||||
#endif
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
status |= set_regout0();
|
||||
}
|
||||
#ifndef USE_BOOTLOADER_MCUBOOT
|
||||
status |= fix_bootloader();
|
||||
#endif
|
||||
return status;
|
||||
}
|
||||
|
||||
static int board_esphome_init() {
|
||||
StatusFlags status = set_uicr();
|
||||
|
||||
#ifdef USE_NRF52_UICR_ERASE
|
||||
if (status & StatusFlags::NEED_ERASE) {
|
||||
nrfx_err_t ret = nrfx_nvmc_uicr_erase();
|
||||
if (ret != NRFX_SUCCESS) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("nrfx_nvmc_uicr_erase failed %d\n", ret);
|
||||
#endif
|
||||
} else {
|
||||
status |= set_uicr();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (status & StatusFlags::NEED_RESET) {
|
||||
/* a reset is required for changes to take effect */
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace esphome::nrf52
|
||||
|
||||
static int board_esphome_init() { return esphome::nrf52::board_esphome_init(); }
|
||||
|
||||
SYS_INIT(board_esphome_init, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
|
||||
|
||||
#endif
|
||||
@@ -23,7 +23,7 @@ void PCF85063Component::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
@@ -23,7 +23,7 @@ void PCF8563Component::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
@@ -35,6 +35,9 @@ DOMAIN = "psram"
|
||||
|
||||
DEPENDENCIES = [PLATFORM_ESP32]
|
||||
|
||||
# PSRAM availability tracking for cross-component coordination
|
||||
KEY_PSRAM_GUARANTEED = "psram_guaranteed"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
psram_ns = cg.esphome_ns.namespace(DOMAIN)
|
||||
@@ -71,6 +74,23 @@ def supported() -> bool:
|
||||
return variant in SPIRAM_MODES
|
||||
|
||||
|
||||
def is_guaranteed() -> bool:
|
||||
"""Check if PSRAM is guaranteed to be available.
|
||||
|
||||
Returns True when PSRAM is configured with both 'disabled: false' and
|
||||
'ignore_not_found: false', meaning the device will fail to boot if PSRAM
|
||||
is not found. This ensures safe use of high buffer configurations that
|
||||
depend on PSRAM.
|
||||
|
||||
This function should be called during code generation (to_code phase) by
|
||||
components that need to know PSRAM availability for configuration decisions.
|
||||
|
||||
Returns:
|
||||
bool: True if PSRAM is guaranteed, False otherwise
|
||||
"""
|
||||
return CORE.data.get(KEY_PSRAM_GUARANTEED, False)
|
||||
|
||||
|
||||
def validate_psram_mode(config):
|
||||
esp32_config = fv.full_config.get()[PLATFORM_ESP32]
|
||||
if config[CONF_SPEED] == "120MHZ":
|
||||
@@ -131,7 +151,22 @@ def get_config_schema(config):
|
||||
|
||||
CONFIG_SCHEMA = get_config_schema
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_psram_mode
|
||||
|
||||
def _store_psram_guaranteed(config):
|
||||
"""Store PSRAM guaranteed status in CORE.data for other components.
|
||||
|
||||
PSRAM is "guaranteed" when it will fail if not found, ensuring safe use
|
||||
of high buffer configurations in network/wifi components.
|
||||
|
||||
Called during final validation to ensure the flag is available
|
||||
before any to_code() functions run.
|
||||
"""
|
||||
psram_guaranteed = not config[CONF_DISABLED] and not config[CONF_IGNORE_NOT_FOUND]
|
||||
CORE.data[KEY_PSRAM_GUARANTEED] = psram_guaranteed
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(validate_psram_mode, _store_psram_guaranteed)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -39,7 +39,7 @@ from esphome.const import (
|
||||
CONF_WAND_ID,
|
||||
CONF_ZERO,
|
||||
)
|
||||
from esphome.core import coroutine
|
||||
from esphome.core import ID, coroutine
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.util import Registry, SimpleRegistry
|
||||
|
||||
@@ -2104,7 +2104,9 @@ async def abbwelcome_action(var, config, args):
|
||||
)
|
||||
cg.add(var.set_data_template(template_))
|
||||
else:
|
||||
cg.add(var.set_data_static(data_))
|
||||
arr_id = ID(f"{var.base}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data_))
|
||||
cg.add(var.set_data_static(arr, len(data_)))
|
||||
|
||||
|
||||
# Mirage
|
||||
|
||||
@@ -214,10 +214,13 @@ template<typename... Ts> class ABBWelcomeAction : public RemoteTransmitterAction
|
||||
TEMPLATABLE_VALUE(uint8_t, message_type)
|
||||
TEMPLATABLE_VALUE(uint8_t, message_id)
|
||||
TEMPLATABLE_VALUE(bool, auto_message_id)
|
||||
void set_data_static(std::vector<uint8_t> data) { data_static_ = std::move(data); }
|
||||
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
|
||||
this->data_func_ = func;
|
||||
has_data_func_ = true;
|
||||
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
this->data_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
void set_data_static(const uint8_t *data, size_t len) {
|
||||
this->data_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
void encode(RemoteTransmitData *dst, Ts... x) override {
|
||||
ABBWelcomeData data;
|
||||
@@ -228,19 +231,25 @@ template<typename... Ts> class ABBWelcomeAction : public RemoteTransmitterAction
|
||||
data.set_message_type(this->message_type_.value(x...));
|
||||
data.set_message_id(this->message_id_.value(x...));
|
||||
data.auto_message_id = this->auto_message_id_.value(x...);
|
||||
if (has_data_func_) {
|
||||
data.set_data(this->data_func_(x...));
|
||||
std::vector<uint8_t> data_vec;
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: copy from flash to vector
|
||||
data_vec.assign(this->data_.data, this->data_.data + this->len_);
|
||||
} else {
|
||||
data.set_data(this->data_static_);
|
||||
// Template mode: call function
|
||||
data_vec = this->data_.func(x...);
|
||||
}
|
||||
data.set_data(data_vec);
|
||||
data.finalize();
|
||||
ABBWelcomeProtocol().encode(dst, data);
|
||||
}
|
||||
|
||||
protected:
|
||||
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||
std::vector<uint8_t> data_static_{};
|
||||
bool has_data_func_{false};
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Data {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} data_;
|
||||
};
|
||||
|
||||
} // namespace remote_base
|
||||
|
||||
@@ -71,6 +71,7 @@ static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0
|
||||
static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL;
|
||||
static const uint16_t PRONTO_DEFAULT_GAP = 45000;
|
||||
static const uint16_t MARK_EXCESS_MICROS = 20;
|
||||
static constexpr size_t PRONTO_LOG_CHUNK_SIZE = 230;
|
||||
|
||||
static uint16_t to_frequency_k_hz(uint16_t code) {
|
||||
if (code == 0)
|
||||
@@ -225,18 +226,18 @@ optional<ProntoData> ProntoProtocol::decode(RemoteReceiveData src) {
|
||||
}
|
||||
|
||||
void ProntoProtocol::dump(const ProntoData &data) {
|
||||
std::string rest;
|
||||
|
||||
rest = data.data;
|
||||
ESP_LOGI(TAG, "Received Pronto: data=");
|
||||
while (true) {
|
||||
ESP_LOGI(TAG, "%s", rest.substr(0, 230).c_str());
|
||||
if (rest.size() > 230) {
|
||||
rest = rest.substr(230);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const char *ptr = data.data.c_str();
|
||||
size_t remaining = data.data.size();
|
||||
|
||||
// Log in chunks, always logging at least once (even for empty string)
|
||||
do {
|
||||
size_t chunk_size = remaining < PRONTO_LOG_CHUNK_SIZE ? remaining : PRONTO_LOG_CHUNK_SIZE;
|
||||
ESP_LOGI(TAG, "%.*s", (int) chunk_size, ptr);
|
||||
ptr += chunk_size;
|
||||
remaining -= chunk_size;
|
||||
} while (remaining > 0);
|
||||
}
|
||||
|
||||
} // namespace remote_base
|
||||
|
||||
@@ -42,17 +42,20 @@ class RawTrigger : public Trigger<RawTimings>, public Component, public RemoteRe
|
||||
|
||||
template<typename... Ts> class RawAction : public RemoteTransmitterActionBase<Ts...> {
|
||||
public:
|
||||
void set_code_template(std::function<RawTimings(Ts...)> func) { this->code_func_ = func; }
|
||||
void set_code_template(RawTimings (*func)(Ts...)) {
|
||||
this->code_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
void set_code_static(const int32_t *code, size_t len) {
|
||||
this->code_static_ = code;
|
||||
this->code_static_len_ = len;
|
||||
this->code_.data = code;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
TEMPLATABLE_VALUE(uint32_t, carrier_frequency);
|
||||
|
||||
void encode(RemoteTransmitData *dst, Ts... x) override {
|
||||
if (this->code_static_ != nullptr) {
|
||||
for (size_t i = 0; i < this->code_static_len_; i++) {
|
||||
auto val = this->code_static_[i];
|
||||
if (this->len_ >= 0) {
|
||||
for (size_t i = 0; i < static_cast<size_t>(this->len_); i++) {
|
||||
auto val = this->code_.data[i];
|
||||
if (val < 0) {
|
||||
dst->space(static_cast<uint32_t>(-val));
|
||||
} else {
|
||||
@@ -60,15 +63,17 @@ template<typename... Ts> class RawAction : public RemoteTransmitterActionBase<Ts
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dst->set_data(this->code_func_(x...));
|
||||
dst->set_data(this->code_.func(x...));
|
||||
}
|
||||
dst->set_carrier_frequency(this->carrier_frequency_.value(x...));
|
||||
}
|
||||
|
||||
protected:
|
||||
std::function<RawTimings(Ts...)> code_func_{nullptr};
|
||||
const int32_t *code_static_{nullptr};
|
||||
int32_t code_static_len_{0};
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Code {
|
||||
RawTimings (*func)(Ts...);
|
||||
const int32_t *data;
|
||||
} code_;
|
||||
};
|
||||
|
||||
class RawDumper : public RemoteReceiverDumperBase {
|
||||
|
||||
0
esphome/components/rx8130/__init__.py
Normal file
0
esphome/components/rx8130/__init__.py
Normal file
128
esphome/components/rx8130/rx8130.cpp
Normal file
128
esphome/components/rx8130/rx8130.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
#include "rx8130.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// https://download.epsondevice.com/td/pdf/app/RX8130CE_en.pdf
|
||||
|
||||
namespace esphome {
|
||||
namespace rx8130 {
|
||||
|
||||
static const uint8_t RX8130_REG_SEC = 0x10;
|
||||
static const uint8_t RX8130_REG_MIN = 0x11;
|
||||
static const uint8_t RX8130_REG_HOUR = 0x12;
|
||||
static const uint8_t RX8130_REG_WDAY = 0x13;
|
||||
static const uint8_t RX8130_REG_MDAY = 0x14;
|
||||
static const uint8_t RX8130_REG_MONTH = 0x15;
|
||||
static const uint8_t RX8130_REG_YEAR = 0x16;
|
||||
static const uint8_t RX8130_REG_EXTEN = 0x1C;
|
||||
static const uint8_t RX8130_REG_FLAG = 0x1D;
|
||||
static const uint8_t RX8130_REG_CTRL0 = 0x1E;
|
||||
static const uint8_t RX8130_REG_CTRL1 = 0x1F;
|
||||
static const uint8_t RX8130_REG_DIG_OFFSET = 0x30;
|
||||
static const uint8_t RX8130_BIT_CTRL_STOP = 0x40;
|
||||
static const uint8_t RX8130_BAT_FLAGS = 0x30;
|
||||
static const uint8_t RX8130_CLEAR_FLAGS = 0x00;
|
||||
|
||||
static const char *const TAG = "rx8130";
|
||||
|
||||
constexpr uint8_t bcd2dec(uint8_t val) { return (val >> 4) * 10 + (val & 0x0f); }
|
||||
constexpr uint8_t dec2bcd(uint8_t val) { return ((val / 10) << 4) + (val % 10); }
|
||||
|
||||
void RX8130Component::setup() {
|
||||
// Set digital offset to disabled with no offset
|
||||
if (this->write_register(RX8130_REG_DIG_OFFSET, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Disable wakeup timers
|
||||
if (this->write_register(RX8130_REG_EXTEN, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Clear VLF flag in case there has been data loss
|
||||
if (this->write_register(RX8130_REG_FLAG, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Clear test flag and disable interrupts
|
||||
if (this->write_register(RX8130_REG_CTRL0, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Enable battery charging and switching
|
||||
if (this->write_register(RX8130_REG_CTRL1, &RX8130_BAT_FLAGS, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Clear STOP bit
|
||||
this->stop_(false);
|
||||
}
|
||||
|
||||
void RX8130Component::update() { this->read_time(); }
|
||||
|
||||
void RX8130Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "RX8130:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
void RX8130Component::read_time() {
|
||||
uint8_t date[7];
|
||||
if (this->read_register(RX8130_REG_SEC, date, 7) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
|
||||
return;
|
||||
}
|
||||
ESPTime rtc_time{
|
||||
.second = bcd2dec(date[0] & 0x7f),
|
||||
.minute = bcd2dec(date[1] & 0x7f),
|
||||
.hour = bcd2dec(date[2] & 0x3f),
|
||||
.day_of_week = bcd2dec(date[3] & 0x7f),
|
||||
.day_of_month = bcd2dec(date[4] & 0x3f),
|
||||
.day_of_year = 1, // ignored by recalc_timestamp_utc(false)
|
||||
.month = bcd2dec(date[5] & 0x1f),
|
||||
.year = static_cast<uint16_t>(bcd2dec(date[6]) + 2000),
|
||||
.is_dst = false, // not used
|
||||
.timestamp = 0 // overwritten by recalc_timestamp_utc(false)
|
||||
};
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Read UTC time: %04d-%02d-%02d %02d:%02d:%02d", rtc_time.year, rtc_time.month, rtc_time.day_of_month,
|
||||
rtc_time.hour, rtc_time.minute, rtc_time.second);
|
||||
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
|
||||
}
|
||||
|
||||
void RX8130Component::write_time() {
|
||||
auto now = time::RealTimeClock::utcnow();
|
||||
if (!now.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid system time, not syncing to RTC.");
|
||||
return;
|
||||
}
|
||||
uint8_t buff[7];
|
||||
buff[0] = dec2bcd(now.second);
|
||||
buff[1] = dec2bcd(now.minute);
|
||||
buff[2] = dec2bcd(now.hour);
|
||||
buff[3] = dec2bcd(now.day_of_week);
|
||||
buff[4] = dec2bcd(now.day_of_month);
|
||||
buff[5] = dec2bcd(now.month);
|
||||
buff[6] = dec2bcd(now.year % 100);
|
||||
this->stop_(true);
|
||||
if (this->write_register(RX8130_REG_SEC, buff, 7) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Wrote UTC time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour,
|
||||
now.minute, now.second);
|
||||
}
|
||||
this->stop_(false);
|
||||
}
|
||||
|
||||
void RX8130Component::stop_(bool stop) {
|
||||
const uint8_t data = stop ? RX8130_BIT_CTRL_STOP : RX8130_CLEAR_FLAGS;
|
||||
if (this->write_register(RX8130_REG_CTRL0, &data, 1) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rx8130
|
||||
} // namespace esphome
|
||||
35
esphome/components/rx8130/rx8130.h
Normal file
35
esphome/components/rx8130/rx8130.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace rx8130 {
|
||||
|
||||
class RX8130Component : public time::RealTimeClock, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
void read_time();
|
||||
void write_time();
|
||||
/// Ensure RTC is initialized at the correct time in the setup sequence
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
protected:
|
||||
void stop_(bool stop);
|
||||
};
|
||||
|
||||
template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<RX8130Component> {
|
||||
public:
|
||||
void play(const Ts... x) override { this->parent_->write_time(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<RX8130Component> {
|
||||
public:
|
||||
void play(const Ts... x) override { this->parent_->read_time(); }
|
||||
};
|
||||
|
||||
} // namespace rx8130
|
||||
} // namespace esphome
|
||||
56
esphome/components/rx8130/time.py
Normal file
56
esphome/components/rx8130/time.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
CODEOWNERS = ["@beormund"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
rx8130_ns = cg.esphome_ns.namespace("rx8130")
|
||||
RX8130Component = rx8130_ns.class_("RX8130Component", time.RealTimeClock, i2c.I2CDevice)
|
||||
WriteAction = rx8130_ns.class_("WriteAction", automation.Action)
|
||||
ReadAction = rx8130_ns.class_("ReadAction", automation.Action)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(RX8130Component),
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0x32))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"rx8130.write_time",
|
||||
WriteAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(RX8130Component),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def rx8130_write_time_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"rx8130.read_time",
|
||||
ReadAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(RX8130Component),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def rx8130_read_time_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
await time.register_time(var, config)
|
||||
@@ -61,6 +61,7 @@ void SNTPComponent::dump_config() {
|
||||
for (auto &server : this->servers_) {
|
||||
ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server);
|
||||
}
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
void SNTPComponent::update() {
|
||||
#if !defined(USE_ESP32)
|
||||
|
||||
@@ -3,7 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import audio, audio_dac
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||
|
||||
AUTO_LOAD = ["audio"]
|
||||
@@ -90,7 +90,10 @@ async def speaker_play_action(config, action_id, template_arg, args):
|
||||
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_data_template(templ))
|
||||
else:
|
||||
cg.add(var.set_data_static(data))
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
||||
cg.add(var.set_data_static(arr, len(data)))
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -10,28 +10,33 @@ namespace speaker {
|
||||
|
||||
template<typename... Ts> class PlayAction : public Action<Ts...>, public Parented<Speaker> {
|
||||
public:
|
||||
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
|
||||
this->data_func_ = func;
|
||||
this->static_ = false;
|
||||
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
this->data_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
void set_data_static(const std::vector<uint8_t> &data) {
|
||||
this->data_static_ = data;
|
||||
this->static_ = true;
|
||||
|
||||
void set_data_static(const uint8_t *data, size_t len) {
|
||||
this->data_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->static_) {
|
||||
this->parent_->play(this->data_static_);
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: pass pointer directly to play(const uint8_t *, size_t)
|
||||
this->parent_->play(this->data_.data, static_cast<size_t>(this->len_));
|
||||
} else {
|
||||
auto val = this->data_func_(x...);
|
||||
// Template mode: call function and pass vector to play(const std::vector<uint8_t> &)
|
||||
auto val = this->data_.func(x...);
|
||||
this->parent_->play(val);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
bool static_{false};
|
||||
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||
std::vector<uint8_t> data_static_{};
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Data {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} data_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Parented<Speaker> {
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
|
||||
from esphome import automation, external_files
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio, esp32, media_player, psram, speaker
|
||||
from esphome.components import audio, esp32, media_player, network, psram, speaker
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
@@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
AUTO_LOAD = ["audio"]
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
CODEOWNERS = ["@kahrendt", "@synesthesiam"]
|
||||
DOMAIN = "media_player"
|
||||
@@ -280,6 +281,18 @@ PIPELINE_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _request_high_performance_networking(config):
|
||||
"""Request high performance networking for streaming media.
|
||||
|
||||
Speaker media player streams audio data, so it always benefits from
|
||||
optimized WiFi and lwip settings regardless of codec support.
|
||||
Called during config validation to ensure flags are set before to_code().
|
||||
"""
|
||||
network.require_high_performance_networking()
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
media_player.media_player_schema(SpeakerMediaPlayer).extend(
|
||||
{
|
||||
@@ -304,6 +317,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.only_with_esp_idf,
|
||||
_validate_repeated_speaker,
|
||||
_request_high_performance_networking,
|
||||
)
|
||||
|
||||
|
||||
@@ -321,28 +335,10 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
|
||||
async def to_code(config):
|
||||
if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]:
|
||||
# Compile all supported audio codecs and optimize the wifi settings
|
||||
|
||||
# Compile all supported audio codecs
|
||||
cg.add_define("USE_AUDIO_FLAC_SUPPORT", True)
|
||||
cg.add_define("USE_AUDIO_MP3_SUPPORT", True)
|
||||
|
||||
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
|
||||
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64)
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64)
|
||||
|
||||
# Allocate wifi buffers in PSRAM
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
|
||||
|
||||
var = await media_player.new_media_player(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID
|
||||
from esphome.core import TimePeriod
|
||||
from esphome.core import ID, TimePeriod
|
||||
|
||||
MULTI_CONF = True
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
@@ -189,7 +189,7 @@ CONFIG_SCHEMA = (
|
||||
cv.GenerateID(): cv.declare_id(SX126x),
|
||||
cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW),
|
||||
cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=600, max=300000),
|
||||
cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Required(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
|
||||
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_CRC_INVERTED, default=True): cv.boolean,
|
||||
@@ -201,7 +201,7 @@ CONFIG_SCHEMA = (
|
||||
cv.hex_int, cv.Range(min=0, max=0xFFFF)
|
||||
),
|
||||
cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000),
|
||||
cv.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000),
|
||||
cv.Required(CONF_HW_VERSION): cv.one_of(
|
||||
"sx1261", "sx1262", "sx1268", "llcc68", lower=True
|
||||
@@ -213,7 +213,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256),
|
||||
cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4),
|
||||
cv.Optional(CONF_PREAMBLE_SIZE, default=8): cv.int_range(min=1, max=65535),
|
||||
cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema,
|
||||
cv.Required(CONF_RST_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_RX_START, default=True): cv.boolean,
|
||||
cv.Required(CONF_RF_SWITCH): cv.boolean,
|
||||
cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING),
|
||||
@@ -329,5 +329,8 @@ async def send_packet_action_to_code(config, action_id, template_arg, args):
|
||||
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_data_template(templ))
|
||||
else:
|
||||
cg.add(var.set_data_static(data))
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
||||
cg.add(var.set_data_static(arr, len(data)))
|
||||
return var
|
||||
|
||||
@@ -14,28 +14,34 @@ template<typename... Ts> class RunImageCalAction : public Action<Ts...>, public
|
||||
|
||||
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<SX126x> {
|
||||
public:
|
||||
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
|
||||
this->data_func_ = func;
|
||||
this->static_ = false;
|
||||
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
this->data_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
|
||||
void set_data_static(const std::vector<uint8_t> &data) {
|
||||
this->data_static_ = data;
|
||||
this->static_ = true;
|
||||
void set_data_static(const uint8_t *data, size_t len) {
|
||||
this->data_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->static_) {
|
||||
this->parent_->transmit_packet(this->data_static_);
|
||||
std::vector<uint8_t> data;
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: copy from flash to vector
|
||||
data.assign(this->data_.data, this->data_.data + this->len_);
|
||||
} else {
|
||||
this->parent_->transmit_packet(this->data_func_(x...));
|
||||
// Template mode: call function
|
||||
data = this->data_.func(x...);
|
||||
}
|
||||
this->parent_->transmit_packet(data);
|
||||
}
|
||||
|
||||
protected:
|
||||
bool static_{false};
|
||||
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||
std::vector<uint8_t> data_static_{};
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Data {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} data_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetModeTxAction : public Action<Ts...>, public Parented<SX126x> {
|
||||
|
||||
@@ -64,7 +64,7 @@ class SX126x : public Component,
|
||||
void dump_config() override;
|
||||
void set_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; }
|
||||
void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; }
|
||||
void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; }
|
||||
void set_busy_pin(GPIOPin *busy_pin) { this->busy_pin_ = busy_pin; }
|
||||
void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; }
|
||||
void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; }
|
||||
void set_crc_inverted(bool crc_inverted) { this->crc_inverted_ = crc_inverted; }
|
||||
@@ -72,7 +72,7 @@ class SX126x : public Component,
|
||||
void set_crc_polynomial(uint16_t crc_polynomial) { this->crc_polynomial_ = crc_polynomial; }
|
||||
void set_crc_initial(uint16_t crc_initial) { this->crc_initial_ = crc_initial; }
|
||||
void set_deviation(uint32_t deviation) { this->deviation_ = deviation; }
|
||||
void set_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; }
|
||||
void set_dio1_pin(GPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; }
|
||||
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
|
||||
void set_hw_version(const std::string &hw_version) { this->hw_version_ = hw_version; }
|
||||
void set_mode_rx();
|
||||
@@ -85,7 +85,7 @@ class SX126x : public Component,
|
||||
void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; }
|
||||
void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; }
|
||||
void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; }
|
||||
void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; }
|
||||
void set_rst_pin(GPIOPin *rst_pin) { this->rst_pin_ = rst_pin; }
|
||||
void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; }
|
||||
void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; }
|
||||
void set_shaping(uint8_t shaping) { this->shaping_ = shaping; }
|
||||
@@ -115,9 +115,9 @@ class SX126x : public Component,
|
||||
std::vector<SX126xListener *> listeners_;
|
||||
std::vector<uint8_t> packet_;
|
||||
std::vector<uint8_t> sync_value_;
|
||||
InternalGPIOPin *busy_pin_{nullptr};
|
||||
InternalGPIOPin *dio1_pin_{nullptr};
|
||||
InternalGPIOPin *rst_pin_{nullptr};
|
||||
GPIOPin *busy_pin_{nullptr};
|
||||
GPIOPin *dio1_pin_{nullptr};
|
||||
GPIOPin *rst_pin_{nullptr};
|
||||
std::string hw_version_;
|
||||
char version_[16];
|
||||
SX126xBw bandwidth_{SX126X_BW_125000};
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID
|
||||
from esphome.core import ID
|
||||
|
||||
MULTI_CONF = True
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
@@ -321,5 +322,8 @@ async def send_packet_action_to_code(config, action_id, template_arg, args):
|
||||
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_data_template(templ))
|
||||
else:
|
||||
cg.add(var.set_data_static(data))
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
||||
cg.add(var.set_data_static(arr, len(data)))
|
||||
return var
|
||||
|
||||
@@ -14,28 +14,34 @@ template<typename... Ts> class RunImageCalAction : public Action<Ts...>, public
|
||||
|
||||
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<SX127x> {
|
||||
public:
|
||||
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
|
||||
this->data_func_ = func;
|
||||
this->static_ = false;
|
||||
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
this->data_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
|
||||
void set_data_static(const std::vector<uint8_t> &data) {
|
||||
this->data_static_ = data;
|
||||
this->static_ = true;
|
||||
void set_data_static(const uint8_t *data, size_t len) {
|
||||
this->data_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->static_) {
|
||||
this->parent_->transmit_packet(this->data_static_);
|
||||
std::vector<uint8_t> data;
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: copy from flash to vector
|
||||
data.assign(this->data_.data, this->data_.data + this->len_);
|
||||
} else {
|
||||
this->parent_->transmit_packet(this->data_func_(x...));
|
||||
// Template mode: call function
|
||||
data = this->data_.func(x...);
|
||||
}
|
||||
this->parent_->transmit_packet(data);
|
||||
}
|
||||
|
||||
protected:
|
||||
bool static_{false};
|
||||
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||
std::vector<uint8_t> data_static_{};
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Data {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} data_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetModeTxAction : public Action<Ts...>, public Parented<SX127x> {
|
||||
|
||||
@@ -945,6 +945,10 @@ async def to_code(config):
|
||||
cg.add(var.set_humidity_hysteresis(config[CONF_HUMIDITY_HYSTERESIS]))
|
||||
|
||||
if CONF_PRESET in config:
|
||||
# Separate standard and custom presets, and build preset config variables
|
||||
standard_presets: list[tuple[cg.MockObj, cg.MockObj]] = []
|
||||
custom_presets: list[tuple[str, cg.MockObj]] = []
|
||||
|
||||
for preset_config in config[CONF_PRESET]:
|
||||
name = preset_config[CONF_NAME]
|
||||
standard_preset = None
|
||||
@@ -987,9 +991,39 @@ async def to_code(config):
|
||||
)
|
||||
|
||||
if standard_preset is not None:
|
||||
cg.add(var.set_preset_config(standard_preset, preset_target_variable))
|
||||
standard_presets.append((standard_preset, preset_target_variable))
|
||||
else:
|
||||
cg.add(var.set_custom_preset_config(name, preset_target_variable))
|
||||
custom_presets.append((name, preset_target_variable))
|
||||
|
||||
# Build initializer list for standard presets
|
||||
if standard_presets:
|
||||
cg.add(
|
||||
var.set_preset_config(
|
||||
[
|
||||
cg.StructInitializer(
|
||||
thermostat_ns.struct("ThermostatPresetEntry"),
|
||||
("preset", preset),
|
||||
("config", preset_var),
|
||||
)
|
||||
for preset, preset_var in standard_presets
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# Build initializer list for custom presets
|
||||
if custom_presets:
|
||||
cg.add(
|
||||
var.set_custom_preset_config(
|
||||
[
|
||||
cg.StructInitializer(
|
||||
thermostat_ns.struct("ThermostatCustomPresetEntry"),
|
||||
("name", cg.RawExpression(f'"{name}"')),
|
||||
("config", preset_var),
|
||||
)
|
||||
for name, preset_var in custom_presets
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if CONF_DEFAULT_PRESET in config:
|
||||
default_preset_name = config[CONF_DEFAULT_PRESET]
|
||||
|
||||
@@ -53,8 +53,8 @@ void ThermostatClimate::setup() {
|
||||
if (use_default_preset) {
|
||||
if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) {
|
||||
this->change_preset_(this->default_preset_);
|
||||
} else if (!this->default_custom_preset_.empty()) {
|
||||
this->change_custom_preset_(this->default_custom_preset_.c_str());
|
||||
} else if (this->default_custom_preset_ != nullptr) {
|
||||
this->change_custom_preset_(this->default_custom_preset_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,16 +319,16 @@ climate::ClimateTraits ThermostatClimate::traits() {
|
||||
if (this->supports_swing_mode_vertical_)
|
||||
traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL);
|
||||
|
||||
for (auto &it : this->preset_config_) {
|
||||
traits.add_supported_preset(it.first);
|
||||
for (const auto &entry : this->preset_config_) {
|
||||
traits.add_supported_preset(entry.preset);
|
||||
}
|
||||
|
||||
// Extract custom preset names from the custom_preset_config_ map
|
||||
// Extract custom preset names from the custom_preset_config_ vector
|
||||
if (!this->custom_preset_config_.empty()) {
|
||||
std::vector<const char *> custom_preset_names;
|
||||
custom_preset_names.reserve(this->custom_preset_config_.size());
|
||||
for (const auto &it : this->custom_preset_config_) {
|
||||
custom_preset_names.push_back(it.first.c_str());
|
||||
for (const auto &entry : this->custom_preset_config_) {
|
||||
custom_preset_names.push_back(entry.name);
|
||||
}
|
||||
traits.set_supported_custom_presets(custom_preset_names);
|
||||
}
|
||||
@@ -1154,12 +1154,18 @@ void ThermostatClimate::dump_preset_config_(const char *preset_name, const Therm
|
||||
}
|
||||
|
||||
void ThermostatClimate::change_preset_(climate::ClimatePreset preset) {
|
||||
auto config = this->preset_config_.find(preset);
|
||||
// Linear search through preset configurations
|
||||
const ThermostatClimateTargetTempConfig *config = nullptr;
|
||||
for (const auto &entry : this->preset_config_) {
|
||||
if (entry.preset == preset) {
|
||||
config = &entry.config;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (config != this->preset_config_.end()) {
|
||||
if (config != nullptr) {
|
||||
ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
|
||||
if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) ||
|
||||
this->preset.value() != preset) {
|
||||
if (this->change_preset_internal_(*config) || (!this->preset.has_value()) || this->preset.value() != preset) {
|
||||
// Fire any preset changed trigger if defined
|
||||
Trigger<> *trig = this->preset_change_trigger_;
|
||||
this->set_preset_(preset);
|
||||
@@ -1178,11 +1184,18 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) {
|
||||
}
|
||||
|
||||
void ThermostatClimate::change_custom_preset_(const char *custom_preset) {
|
||||
auto config = this->custom_preset_config_.find(custom_preset);
|
||||
// Linear search through custom preset configurations
|
||||
const ThermostatClimateTargetTempConfig *config = nullptr;
|
||||
for (const auto &entry : this->custom_preset_config_) {
|
||||
if (strcmp(entry.name, custom_preset) == 0) {
|
||||
config = &entry.config;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (config != this->custom_preset_config_.end()) {
|
||||
if (config != nullptr) {
|
||||
ESP_LOGV(TAG, "Custom preset %s requested", custom_preset);
|
||||
if (this->change_preset_internal_(config->second) || !this->has_custom_preset() ||
|
||||
if (this->change_preset_internal_(*config) || !this->has_custom_preset() ||
|
||||
strcmp(this->get_custom_preset(), custom_preset) != 0) {
|
||||
// Fire any preset changed trigger if defined
|
||||
Trigger<> *trig = this->preset_change_trigger_;
|
||||
@@ -1247,14 +1260,12 @@ bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTem
|
||||
return something_changed;
|
||||
}
|
||||
|
||||
void ThermostatClimate::set_preset_config(climate::ClimatePreset preset,
|
||||
const ThermostatClimateTargetTempConfig &config) {
|
||||
this->preset_config_[preset] = config;
|
||||
void ThermostatClimate::set_preset_config(std::initializer_list<PresetEntry> presets) {
|
||||
this->preset_config_ = presets;
|
||||
}
|
||||
|
||||
void ThermostatClimate::set_custom_preset_config(const std::string &name,
|
||||
const ThermostatClimateTargetTempConfig &config) {
|
||||
this->custom_preset_config_[name] = config;
|
||||
void ThermostatClimate::set_custom_preset_config(std::initializer_list<CustomPresetEntry> presets) {
|
||||
this->custom_preset_config_ = presets;
|
||||
}
|
||||
|
||||
ThermostatClimate::ThermostatClimate()
|
||||
@@ -1293,8 +1304,16 @@ ThermostatClimate::ThermostatClimate()
|
||||
humidity_control_humidify_action_trigger_(new Trigger<>()),
|
||||
humidity_control_off_action_trigger_(new Trigger<>()) {}
|
||||
|
||||
void ThermostatClimate::set_default_preset(const std::string &custom_preset) {
|
||||
this->default_custom_preset_ = custom_preset;
|
||||
void ThermostatClimate::set_default_preset(const char *custom_preset) {
|
||||
// Find the preset in custom_preset_config_ and store pointer from there
|
||||
for (const auto &entry : this->custom_preset_config_) {
|
||||
if (strcmp(entry.name, custom_preset) == 0) {
|
||||
this->default_custom_preset_ = entry.name;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If not found, it will be caught during validation
|
||||
this->default_custom_preset_ = nullptr;
|
||||
}
|
||||
|
||||
void ThermostatClimate::set_default_preset(climate::ClimatePreset preset) { this->default_preset_ = preset; }
|
||||
@@ -1605,19 +1624,22 @@ void ThermostatClimate::dump_config() {
|
||||
|
||||
if (!this->preset_config_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " Supported PRESETS:");
|
||||
for (auto &it : this->preset_config_) {
|
||||
const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first));
|
||||
ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_preset_ ? " (default)" : "");
|
||||
this->dump_preset_config_(preset_name, it.second);
|
||||
for (const auto &entry : this->preset_config_) {
|
||||
const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(entry.preset));
|
||||
ESP_LOGCONFIG(TAG, " %s:%s", preset_name, entry.preset == this->default_preset_ ? " (default)" : "");
|
||||
this->dump_preset_config_(preset_name, entry.config);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->custom_preset_config_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS:");
|
||||
for (auto &it : this->custom_preset_config_) {
|
||||
const auto *preset_name = it.first.c_str();
|
||||
ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_custom_preset_ ? " (default)" : "");
|
||||
this->dump_preset_config_(preset_name, it.second);
|
||||
for (const auto &entry : this->custom_preset_config_) {
|
||||
const auto *preset_name = entry.name;
|
||||
ESP_LOGCONFIG(TAG, " %s:%s", preset_name,
|
||||
(this->default_custom_preset_ != nullptr && strcmp(entry.name, this->default_custom_preset_) == 0)
|
||||
? " (default)"
|
||||
: "");
|
||||
this->dump_preset_config_(preset_name, entry.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
#include <map>
|
||||
|
||||
namespace esphome {
|
||||
namespace thermostat {
|
||||
@@ -72,14 +72,29 @@ struct ThermostatClimateTargetTempConfig {
|
||||
optional<climate::ClimateMode> mode_{};
|
||||
};
|
||||
|
||||
/// Entry for standard preset lookup
|
||||
struct ThermostatPresetEntry {
|
||||
climate::ClimatePreset preset;
|
||||
ThermostatClimateTargetTempConfig config;
|
||||
};
|
||||
|
||||
/// Entry for custom preset lookup
|
||||
struct ThermostatCustomPresetEntry {
|
||||
const char *name;
|
||||
ThermostatClimateTargetTempConfig config;
|
||||
};
|
||||
|
||||
class ThermostatClimate : public climate::Climate, public Component {
|
||||
public:
|
||||
using PresetEntry = ThermostatPresetEntry;
|
||||
using CustomPresetEntry = ThermostatCustomPresetEntry;
|
||||
|
||||
ThermostatClimate();
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
void set_default_preset(const std::string &custom_preset);
|
||||
void set_default_preset(const char *custom_preset);
|
||||
void set_default_preset(climate::ClimatePreset preset);
|
||||
void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from);
|
||||
void set_set_point_minimum_differential(float differential);
|
||||
@@ -131,8 +146,8 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
void set_supports_humidification(bool supports_humidification);
|
||||
void set_supports_two_points(bool supports_two_points);
|
||||
|
||||
void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config);
|
||||
void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config);
|
||||
void set_preset_config(std::initializer_list<PresetEntry> presets);
|
||||
void set_custom_preset_config(std::initializer_list<CustomPresetEntry> presets);
|
||||
|
||||
Trigger<> *get_cool_action_trigger() const;
|
||||
Trigger<> *get_supplemental_cool_action_trigger() const;
|
||||
@@ -516,9 +531,6 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
Trigger<> *prev_swing_mode_trigger_{nullptr};
|
||||
Trigger<> *prev_humidity_control_trigger_{nullptr};
|
||||
|
||||
/// Default custom preset to use on start up
|
||||
std::string default_custom_preset_{};
|
||||
|
||||
/// Climate action timers
|
||||
std::array<ThermostatClimateTimer, THERMOSTAT_TIMER_COUNT> timer_{
|
||||
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)),
|
||||
@@ -534,9 +546,12 @@ class ThermostatClimate : public climate::Climate, public Component {
|
||||
};
|
||||
|
||||
/// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc)
|
||||
std::map<climate::ClimatePreset, ThermostatClimateTargetTempConfig> preset_config_{};
|
||||
FixedVector<PresetEntry> preset_config_{};
|
||||
/// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset")
|
||||
std::map<std::string, ThermostatClimateTargetTempConfig> custom_preset_config_{};
|
||||
FixedVector<CustomPresetEntry> custom_preset_config_{};
|
||||
/// Default custom preset to use on start up (pointer to entry in custom_preset_config_)
|
||||
private:
|
||||
const char *default_custom_preset_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace thermostat
|
||||
|
||||
@@ -23,6 +23,13 @@ namespace time {
|
||||
static const char *const TAG = "time";
|
||||
|
||||
RealTimeClock::RealTimeClock() = default;
|
||||
|
||||
void RealTimeClock::dump_config() {
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str());
|
||||
#endif
|
||||
}
|
||||
|
||||
void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch);
|
||||
// Update UTC epoch time.
|
||||
|
||||
@@ -52,6 +52,8 @@ class RealTimeClock : public PollingComponent {
|
||||
this->time_sync_callback_.add(std::move(callback));
|
||||
};
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
/// Report a unix epoch as current time.
|
||||
void synchronize_epoch_(uint32_t epoch);
|
||||
|
||||
@@ -31,7 +31,7 @@ from esphome.const import (
|
||||
PLATFORM_HOST,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, ID
|
||||
import esphome.final_validate as fv
|
||||
from esphome.yaml_util import make_data_base
|
||||
|
||||
@@ -446,7 +446,10 @@ async def uart_write_to_code(config, action_id, template_arg, args):
|
||||
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_data_template(templ))
|
||||
else:
|
||||
cg.add(var.set_data_static(cg.ArrayInitializer(*data)))
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
||||
cg.add(var.set_data_static(arr, len(data)))
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -10,32 +10,35 @@ namespace uart {
|
||||
|
||||
template<typename... Ts> class UARTWriteAction : public Action<Ts...>, public Parented<UARTComponent> {
|
||||
public:
|
||||
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
|
||||
this->data_func_ = func;
|
||||
this->static_ = false;
|
||||
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
// Stateless lambdas (generated by ESPHome) implicitly convert to function pointers
|
||||
this->code_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
void set_data_static(std::vector<uint8_t> &&data) {
|
||||
this->data_static_ = std::move(data);
|
||||
this->static_ = true;
|
||||
}
|
||||
void set_data_static(std::initializer_list<uint8_t> data) {
|
||||
this->data_static_ = std::vector<uint8_t>(data);
|
||||
this->static_ = true;
|
||||
|
||||
// Store pointer to static data in flash (no RAM copy)
|
||||
void set_data_static(const uint8_t *data, size_t len) {
|
||||
this->code_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->static_) {
|
||||
this->parent_->write_array(this->data_static_);
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: use pointer and length
|
||||
this->parent_->write_array(this->code_.data, static_cast<size_t>(this->len_));
|
||||
} else {
|
||||
auto val = this->data_func_(x...);
|
||||
// Template mode: call function
|
||||
auto val = this->code_.func(x...);
|
||||
this->parent_->write_array(val);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
bool static_{false};
|
||||
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||
std::vector<uint8_t> data_static_{};
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Code {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} code_;
|
||||
};
|
||||
|
||||
} // namespace uart
|
||||
|
||||
@@ -12,7 +12,7 @@ from esphome.components.packet_transport import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
|
||||
from esphome.core import Lambda
|
||||
from esphome.core import ID, Lambda
|
||||
from esphome.cpp_generator import ExpressionStatement, MockObj
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
@@ -158,5 +158,8 @@ async def udp_write_to_code(config, action_id, template_arg, args):
|
||||
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_data_template(templ))
|
||||
else:
|
||||
cg.add(var.set_data_static(data))
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
||||
cg.add(var.set_data_static(arr, len(data)))
|
||||
return var
|
||||
|
||||
@@ -11,28 +11,33 @@ namespace udp {
|
||||
|
||||
template<typename... Ts> class UDPWriteAction : public Action<Ts...>, public Parented<UDPComponent> {
|
||||
public:
|
||||
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
|
||||
this->data_func_ = func;
|
||||
this->static_ = false;
|
||||
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
this->data_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
void set_data_static(const std::vector<uint8_t> &data) {
|
||||
this->data_static_ = data;
|
||||
this->static_ = true;
|
||||
|
||||
void set_data_static(const uint8_t *data, size_t len) {
|
||||
this->data_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->static_) {
|
||||
this->parent_->send_packet(this->data_static_);
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: pass pointer directly to send_packet(const uint8_t *, size_t)
|
||||
this->parent_->send_packet(this->data_.data, static_cast<size_t>(this->len_));
|
||||
} else {
|
||||
auto val = this->data_func_(x...);
|
||||
// Template mode: call function and pass vector to send_packet(const std::vector<uint8_t> &)
|
||||
auto val = this->data_.func(x...);
|
||||
this->parent_->send_packet(val);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
bool static_{false};
|
||||
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||
std::vector<uint8_t> data_static_{};
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Data {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} data_;
|
||||
};
|
||||
|
||||
} // namespace udp
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_USE_PSRAM
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
|
||||
from esphome.components.network import ip_address_literal
|
||||
from esphome.components.network import (
|
||||
has_high_performance_networking,
|
||||
ip_address_literal,
|
||||
)
|
||||
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import only_with_esp_idf
|
||||
@@ -42,6 +48,7 @@ from esphome.const import (
|
||||
CONF_TTLS_PHASE_2,
|
||||
CONF_USE_ADDRESS,
|
||||
CONF_USERNAME,
|
||||
Platform,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority
|
||||
@@ -49,10 +56,15 @@ import esphome.final_validate as fv
|
||||
|
||||
from . import wpa2_eap
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTO_LOAD = ["network"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4]
|
||||
CONF_SAVE = "save"
|
||||
CONF_MIN_AUTH_MODE = "min_auth_mode"
|
||||
|
||||
# Maximum number of WiFi networks that can be configured
|
||||
# Limited to 127 because selected_sta_index_ is int8_t in C++
|
||||
@@ -70,6 +82,14 @@ WIFI_POWER_SAVE_MODES = {
|
||||
"LIGHT": WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT,
|
||||
"HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH,
|
||||
}
|
||||
|
||||
WifiMinAuthMode = wifi_ns.enum("WifiMinAuthMode")
|
||||
WIFI_MIN_AUTH_MODES = {
|
||||
"WPA": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA,
|
||||
"WPA2": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA2,
|
||||
"WPA3": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA3,
|
||||
}
|
||||
VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True)
|
||||
WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
|
||||
WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
|
||||
WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action)
|
||||
@@ -174,7 +194,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend(
|
||||
{
|
||||
cv.Optional(CONF_BSSID): cv.mac_address,
|
||||
cv.Optional(CONF_HIDDEN): cv.boolean,
|
||||
cv.Optional(CONF_PRIORITY, default=0.0): cv.float_,
|
||||
cv.Optional(CONF_PRIORITY, default=0): cv.int_range(min=-128, max=127),
|
||||
cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA,
|
||||
}
|
||||
)
|
||||
@@ -187,6 +207,27 @@ def validate_variant(_):
|
||||
raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}")
|
||||
|
||||
|
||||
def _apply_min_auth_mode_default(config):
|
||||
"""Apply platform-specific default for min_auth_mode and warn ESP8266 users."""
|
||||
# Only apply defaults for platforms that support min_auth_mode
|
||||
if CONF_MIN_AUTH_MODE not in config and (CORE.is_esp8266 or CORE.is_esp32):
|
||||
if CORE.is_esp8266:
|
||||
_LOGGER.warning(
|
||||
"The minimum WiFi authentication mode (wifi -> min_auth_mode) is not set. "
|
||||
"This controls the weakest encryption your device will accept when connecting to WiFi. "
|
||||
"Currently defaults to WPA (less secure), but will change to WPA2 (more secure) in 2026.6.0. "
|
||||
"WPA uses TKIP encryption which has known security vulnerabilities and should be avoided. "
|
||||
"WPA2 uses AES encryption which is significantly more secure. "
|
||||
"To silence this warning, explicitly set min_auth_mode under 'wifi:'. "
|
||||
"If your router supports WPA2 or WPA3, set 'min_auth_mode: WPA2'. "
|
||||
"If your router only supports WPA, set 'min_auth_mode: WPA'."
|
||||
)
|
||||
config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA")
|
||||
elif CORE.is_esp32:
|
||||
config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA2")
|
||||
return config
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
has_sta = bool(config.get(CONF_NETWORKS, True))
|
||||
has_ap = CONF_AP in config
|
||||
@@ -287,6 +328,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
|
||||
cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||
cv.Optional(CONF_MIN_AUTH_MODE): cv.All(
|
||||
VALIDATE_WIFI_MIN_AUTH_MODE,
|
||||
cv.only_on([Platform.ESP32, Platform.ESP8266]),
|
||||
),
|
||||
cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
|
||||
cv.decibel, cv.float_range(min=8.5, max=20.5)
|
||||
),
|
||||
@@ -311,6 +356,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
),
|
||||
_apply_min_auth_mode_default,
|
||||
_validate,
|
||||
)
|
||||
|
||||
@@ -385,6 +431,8 @@ async def to_code(config):
|
||||
|
||||
# Track if any network uses Enterprise authentication
|
||||
has_eap = False
|
||||
# Track if any network uses manual IP
|
||||
has_manual_ip = False
|
||||
|
||||
# Initialize FixedVector with the count of networks
|
||||
networks = config.get(CONF_NETWORKS, [])
|
||||
@@ -398,11 +446,15 @@ async def to_code(config):
|
||||
for network in networks:
|
||||
if CONF_EAP in network:
|
||||
has_eap = True
|
||||
if network.get(CONF_MANUAL_IP) or config.get(CONF_MANUAL_IP):
|
||||
has_manual_ip = True
|
||||
cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
|
||||
|
||||
if CONF_AP in config:
|
||||
conf = config[CONF_AP]
|
||||
ip_config = conf.get(CONF_MANUAL_IP)
|
||||
if ip_config:
|
||||
has_manual_ip = True
|
||||
cg.with_local_variable(
|
||||
conf[CONF_ID],
|
||||
WiFiAP(),
|
||||
@@ -418,8 +470,14 @@ async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap)
|
||||
|
||||
# Only define USE_WIFI_MANUAL_IP if any AP uses manual IP
|
||||
if has_manual_ip:
|
||||
cg.add_define("USE_WIFI_MANUAL_IP")
|
||||
|
||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
|
||||
if CONF_MIN_AUTH_MODE in config:
|
||||
cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE]))
|
||||
if config[CONF_FAST_CONNECT]:
|
||||
cg.add_define("USE_WIFI_FAST_CONNECT")
|
||||
cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN]))
|
||||
@@ -444,6 +502,56 @@ async def to_code(config):
|
||||
|
||||
if config.get(CONF_USE_PSRAM):
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
|
||||
|
||||
# Apply high performance WiFi settings if high performance networking is enabled
|
||||
if CORE.is_esp32 and CORE.using_esp_idf and has_high_performance_networking():
|
||||
# Check if PSRAM is guaranteed (set by psram component during final validation)
|
||||
psram_guaranteed = psram_is_guaranteed()
|
||||
|
||||
# Always allocate WiFi buffers in PSRAM if available
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
|
||||
|
||||
if psram_guaranteed:
|
||||
_LOGGER.info(
|
||||
"Applying high-performance WiFi settings (PSRAM guaranteed): 512 RX buffers, 32 TX buffers"
|
||||
)
|
||||
# PSRAM is guaranteed - use aggressive settings
|
||||
# Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true in networking component
|
||||
# Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702
|
||||
|
||||
# Large dynamic RX buffers (requires PSRAM)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 512)
|
||||
|
||||
# Static TX buffers for better performance
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BUFFER_TYPE", 0)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM", 32)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM", 8)
|
||||
|
||||
# AMPDU settings optimized for PSRAM
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 16)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Applying optimized WiFi settings: 64 RX buffers, 64 TX buffers"
|
||||
)
|
||||
# PSRAM not guaranteed - use more conservative, but still optimized settings
|
||||
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
|
||||
|
||||
# Standard buffer counts
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64)
|
||||
|
||||
# Standard AMPDU settings
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
|
||||
|
||||
cg.add_define("USE_WIFI")
|
||||
|
||||
# must register before OTA safe mode check
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -74,12 +74,6 @@ enum WiFiComponentState : uint8_t {
|
||||
WIFI_COMPONENT_STATE_STA_SCANNING,
|
||||
/** WiFi is in STA(+AP) mode and currently connecting to an AP. */
|
||||
WIFI_COMPONENT_STATE_STA_CONNECTING,
|
||||
/** WiFi is in STA(+AP) mode and currently connecting to an AP a second time.
|
||||
*
|
||||
* This is required because for some reason ESPs don't like to connect to WiFi APs directly after
|
||||
* a scan.
|
||||
* */
|
||||
WIFI_COMPONENT_STATE_STA_CONNECTING_2,
|
||||
/** WiFi is in STA(+AP) mode and successfully connected. */
|
||||
WIFI_COMPONENT_STATE_STA_CONNECTED,
|
||||
/** WiFi is in AP-only mode and internal AP is already enabled. */
|
||||
@@ -94,6 +88,24 @@ enum class WiFiSTAConnectStatus : int {
|
||||
ERROR_CONNECT_FAILED,
|
||||
};
|
||||
|
||||
/// Tracks the current retry strategy/phase for WiFi connection attempts
|
||||
enum class WiFiRetryPhase : uint8_t {
|
||||
/// Initial connection attempt (varies based on fast_connect setting)
|
||||
INITIAL_CONNECT,
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
/// Fast connect mode: cycling through configured APs (config-only, no scan)
|
||||
FAST_CONNECT_CYCLING_APS,
|
||||
#endif
|
||||
/// Explicitly hidden networks (user marked as hidden, try before scanning)
|
||||
EXPLICIT_HIDDEN,
|
||||
/// Scan-based: connecting to best AP from scan results
|
||||
SCAN_CONNECTING,
|
||||
/// Retry networks not found in scan (might be hidden)
|
||||
RETRY_HIDDEN,
|
||||
/// Restarting WiFi adapter to clear stuck state
|
||||
RESTARTING_ADAPTER,
|
||||
};
|
||||
|
||||
/// Struct for setting static IPs in WiFiComponent.
|
||||
struct ManualIP {
|
||||
network::IPAddress static_ip;
|
||||
@@ -139,8 +151,10 @@ class WiFiAP {
|
||||
void set_eap(optional<EAPAuth> eap_auth);
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
void set_channel(optional<uint8_t> channel);
|
||||
void set_priority(float priority) { priority_ = priority; }
|
||||
void set_priority(int8_t priority) { priority_ = priority; }
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
void set_manual_ip(optional<ManualIP> manual_ip);
|
||||
#endif
|
||||
void set_hidden(bool hidden);
|
||||
const std::string &get_ssid() const;
|
||||
const optional<bssid_t> &get_bssid() const;
|
||||
@@ -149,8 +163,10 @@ class WiFiAP {
|
||||
const optional<EAPAuth> &get_eap() const;
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
const optional<uint8_t> &get_channel() const;
|
||||
float get_priority() const { return priority_; }
|
||||
int8_t get_priority() const { return priority_; }
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
const optional<ManualIP> &get_manual_ip() const;
|
||||
#endif
|
||||
bool get_hidden() const;
|
||||
|
||||
protected:
|
||||
@@ -160,9 +176,11 @@ class WiFiAP {
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
optional<EAPAuth> eap_;
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
optional<ManualIP> manual_ip_;
|
||||
float priority_{0};
|
||||
#endif
|
||||
optional<uint8_t> channel_;
|
||||
int8_t priority_{0};
|
||||
bool hidden_{false};
|
||||
};
|
||||
|
||||
@@ -180,17 +198,17 @@ class WiFiScanResult {
|
||||
int8_t get_rssi() const;
|
||||
bool get_with_auth() const;
|
||||
bool get_is_hidden() const;
|
||||
float get_priority() const { return priority_; }
|
||||
void set_priority(float priority) { priority_ = priority; }
|
||||
int8_t get_priority() const { return priority_; }
|
||||
void set_priority(int8_t priority) { priority_ = priority; }
|
||||
|
||||
bool operator==(const WiFiScanResult &rhs) const;
|
||||
|
||||
protected:
|
||||
bssid_t bssid_;
|
||||
std::string ssid_;
|
||||
float priority_{0.0f};
|
||||
uint8_t channel_;
|
||||
int8_t rssi_;
|
||||
std::string ssid_;
|
||||
int8_t priority_{0};
|
||||
bool matches_{false};
|
||||
bool with_auth_;
|
||||
bool is_hidden_;
|
||||
@@ -198,7 +216,7 @@ class WiFiScanResult {
|
||||
|
||||
struct WiFiSTAPriority {
|
||||
bssid_t bssid;
|
||||
float priority;
|
||||
int8_t priority;
|
||||
};
|
||||
|
||||
enum WiFiPowerSaveMode : uint8_t {
|
||||
@@ -207,6 +225,12 @@ enum WiFiPowerSaveMode : uint8_t {
|
||||
WIFI_POWER_SAVE_HIGH,
|
||||
};
|
||||
|
||||
enum WifiMinAuthMode : uint8_t {
|
||||
WIFI_MIN_AUTH_MODE_WPA = 0,
|
||||
WIFI_MIN_AUTH_MODE_WPA2,
|
||||
WIFI_MIN_AUTH_MODE_WPA3,
|
||||
};
|
||||
|
||||
#ifdef USE_ESP32
|
||||
struct IDFWiFiEvent;
|
||||
#endif
|
||||
@@ -245,19 +269,20 @@ class WiFiComponent : public Component {
|
||||
bool is_disabled();
|
||||
void start_scanning();
|
||||
void check_scanning_finished();
|
||||
void start_connecting(const WiFiAP &ap, bool two);
|
||||
void start_connecting(const WiFiAP &ap);
|
||||
// Backward compatibility overload - ignores 'two' parameter
|
||||
void start_connecting(const WiFiAP &ap, bool /* two */) { this->start_connecting(ap); }
|
||||
|
||||
void check_connecting_finished();
|
||||
|
||||
void retry_connect();
|
||||
|
||||
bool can_proceed() override;
|
||||
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
|
||||
bool is_connected();
|
||||
|
||||
void set_power_save_mode(WiFiPowerSaveMode power_save);
|
||||
void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; }
|
||||
void set_output_power(float output_power) { output_power_ = output_power; }
|
||||
|
||||
void set_passive_scan(bool passive);
|
||||
@@ -301,14 +326,14 @@ class WiFiComponent : public Component {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
float get_sta_priority(const bssid_t bssid) {
|
||||
int8_t get_sta_priority(const bssid_t bssid) {
|
||||
for (auto &it : this->sta_priorities_) {
|
||||
if (it.bssid == bssid)
|
||||
return it.priority;
|
||||
}
|
||||
return 0.0f;
|
||||
return 0;
|
||||
}
|
||||
void set_sta_priority(const bssid_t bssid, float priority) {
|
||||
void set_sta_priority(const bssid_t bssid, int8_t priority) {
|
||||
for (auto &it : this->sta_priorities_) {
|
||||
if (it.bssid == bssid) {
|
||||
it.priority = priority;
|
||||
@@ -341,8 +366,38 @@ class WiFiComponent : public Component {
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
void print_connect_params_();
|
||||
WiFiAP build_wifi_ap_from_selected_() const;
|
||||
WiFiAP build_params_for_current_phase_();
|
||||
|
||||
/// Determine next retry phase based on current state and failure conditions
|
||||
WiFiRetryPhase determine_next_phase_();
|
||||
/// Transition to a new retry phase with logging
|
||||
/// Returns true if a scan was started (caller should wait), false otherwise
|
||||
bool transition_to_phase_(WiFiRetryPhase new_phase);
|
||||
/// Check if we need valid scan results for the current phase but don't have any
|
||||
/// Returns true if the phase requires scan results but they're missing or don't match
|
||||
bool needs_scan_results_() const;
|
||||
/// Check if we went through EXPLICIT_HIDDEN phase (first network is marked hidden)
|
||||
/// Used in RETRY_HIDDEN to determine whether to skip explicitly hidden networks
|
||||
bool went_through_explicit_hidden_phase_() const;
|
||||
/// Find the index of the first non-hidden network
|
||||
/// Returns where EXPLICIT_HIDDEN phase would have stopped, or -1 if all networks are hidden
|
||||
int8_t find_first_non_hidden_index_() const;
|
||||
/// Check if an SSID was seen in the most recent scan results
|
||||
/// Used to skip hidden mode for SSIDs we know are visible
|
||||
bool ssid_was_seen_in_scan_(const std::string &ssid) const;
|
||||
/// Find next SSID that wasn't in scan results (might be hidden)
|
||||
/// Returns index of next potentially hidden SSID, or -1 if none found
|
||||
/// @param start_index Start searching from index after this (-1 to start from beginning)
|
||||
int8_t find_next_hidden_sta_(int8_t start_index);
|
||||
/// Log failed connection and decrease BSSID priority to avoid repeated attempts
|
||||
void log_and_adjust_priority_for_failed_connect_();
|
||||
/// Clear BSSID priority tracking if all priorities are at minimum (saves memory)
|
||||
void clear_priorities_if_all_min_();
|
||||
/// Advance to next target (AP/SSID) within current phase, or increment retry counter
|
||||
/// Called when staying in the same phase after a failed connection attempt
|
||||
void advance_to_next_target_or_increment_retry_();
|
||||
/// Start initial connection - either scan or connect directly to hidden networks
|
||||
void start_initial_connection_();
|
||||
const WiFiAP *get_selected_sta_() const {
|
||||
if (this->selected_sta_index_ >= 0 && static_cast<size_t>(this->selected_sta_index_) < this->sta_.size()) {
|
||||
return &this->sta_[this->selected_sta_index_];
|
||||
@@ -356,14 +411,15 @@ class WiFiComponent : public Component {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
// Reset state for next fast connect AP attempt
|
||||
// Clears old scan data so the new AP is tried with config only (SSID without specific BSSID/channel)
|
||||
void reset_for_next_ap_attempt_() {
|
||||
this->num_retried_ = 0;
|
||||
this->scan_result_.clear();
|
||||
bool all_networks_hidden_() const {
|
||||
if (this->sta_.empty())
|
||||
return false;
|
||||
for (const auto &ap : this->sta_) {
|
||||
if (!ap.get_hidden())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void wifi_loop_();
|
||||
bool wifi_mode_(optional<bool> sta, optional<bool> ap);
|
||||
@@ -443,20 +499,19 @@ class WiFiComponent : public Component {
|
||||
// Group all 8-bit values together
|
||||
WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF};
|
||||
WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
|
||||
WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2};
|
||||
WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT};
|
||||
uint8_t num_retried_{0};
|
||||
// Index into sta_ array for the currently selected AP configuration (-1 = none selected)
|
||||
// Used to access password, manual_ip, priority, EAP settings, and hidden flag
|
||||
// int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS)
|
||||
int8_t selected_sta_index_{-1};
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
uint8_t num_ipv6_addresses_{0};
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
// Group all boolean values together
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
bool trying_loaded_ap_{false};
|
||||
#endif
|
||||
bool retry_hidden_{false};
|
||||
bool has_ap_{false};
|
||||
bool handled_connected_state_{false};
|
||||
bool error_from_callback_{false};
|
||||
|
||||
@@ -258,8 +258,17 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
if (ap.get_password().empty()) {
|
||||
conf.threshold.authmode = AUTH_OPEN;
|
||||
} else {
|
||||
// Only allow auth modes with at least WPA
|
||||
conf.threshold.authmode = AUTH_WPA_PSK;
|
||||
// Set threshold based on configured minimum auth mode
|
||||
// Note: ESP8266 doesn't support WPA3
|
||||
switch (this->min_auth_mode_) {
|
||||
case WIFI_MIN_AUTH_MODE_WPA:
|
||||
conf.threshold.authmode = AUTH_WPA_PSK;
|
||||
break;
|
||||
case WIFI_MIN_AUTH_MODE_WPA2:
|
||||
case WIFI_MIN_AUTH_MODE_WPA3: // Fall back to WPA2 for ESP8266
|
||||
conf.threshold.authmode = AUTH_WPA2_PSK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
conf.threshold.rssi = -127;
|
||||
#endif
|
||||
@@ -273,9 +282,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!this->wifi_sta_ip_config_({})) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// setup enterprise authentication if required
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
@@ -823,10 +838,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!this->wifi_ap_ip_config_({})) {
|
||||
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -308,7 +308,18 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
if (ap.get_password().empty()) {
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
||||
} else {
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK;
|
||||
// Set threshold based on configured minimum auth mode
|
||||
switch (this->min_auth_mode_) {
|
||||
case WIFI_MIN_AUTH_MODE_WPA:
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA_PSK;
|
||||
break;
|
||||
case WIFI_MIN_AUTH_MODE_WPA2:
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
|
||||
break;
|
||||
case WIFI_MIN_AUTH_MODE_WPA3:
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
@@ -347,8 +358,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
// The minimum rssi to accept in the fast scan mode
|
||||
conf.sta.threshold.rssi = -127;
|
||||
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
||||
|
||||
wifi_config_t current_conf;
|
||||
esp_err_t err;
|
||||
err = esp_wifi_get_config(WIFI_IF_STA, ¤t_conf);
|
||||
@@ -371,9 +380,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!this->wifi_sta_ip_config_({})) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// setup enterprise authentication if required
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
@@ -985,10 +1000,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!this->wifi_ap_ip_config_({})) {
|
||||
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -112,9 +112,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
WiFi.disconnect();
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!this->wifi_sta_ip_config_({})) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->wifi_apply_hostname_();
|
||||
|
||||
@@ -445,10 +451,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
if (!this->wifi_mode_({}, true))
|
||||
return false;
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!this->wifi_ap_ip_config_({})) {
|
||||
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
yield();
|
||||
|
||||
|
||||
@@ -55,8 +55,13 @@ bool WiFiComponent::wifi_apply_power_save_() {
|
||||
bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; }
|
||||
|
||||
bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_sta_ip_config_(ap.get_manual_ip()))
|
||||
return false;
|
||||
#else
|
||||
if (!this->wifi_sta_ip_config_({}))
|
||||
return false;
|
||||
#endif
|
||||
|
||||
auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str());
|
||||
if (ret != WL_CONNECTED)
|
||||
@@ -161,10 +166,17 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
||||
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
if (!this->wifi_mode_({}, true))
|
||||
return false;
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!this->wifi_ap_ip_config_({})) {
|
||||
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1));
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <zephyr/random/random.h>
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -25,7 +26,14 @@ void arch_init() {
|
||||
wdt_config.window.max = 2000;
|
||||
wdt_channel_id = wdt_install_timeout(WDT, &wdt_config);
|
||||
if (wdt_channel_id >= 0) {
|
||||
wdt_setup(WDT, WDT_OPT_PAUSE_HALTED_BY_DBG | WDT_OPT_PAUSE_IN_SLEEP);
|
||||
uint8_t options = 0;
|
||||
#ifdef USE_DEBUG
|
||||
options |= WDT_OPT_PAUSE_HALTED_BY_DBG;
|
||||
#endif
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
options |= WDT_OPT_PAUSE_IN_SLEEP;
|
||||
#endif
|
||||
wdt_setup(WDT, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.11.0-dev"
|
||||
__version__ = "2025.11.0b1"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -412,7 +412,12 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
|
||||
|
||||
void setup() override {
|
||||
// Start with loop disabled - only enable when there's work to do
|
||||
this->disable_loop();
|
||||
// IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already
|
||||
// called before our setup() (e.g., from on_boot trigger at same priority level)
|
||||
// and we must not undo its enable_loop() call
|
||||
if (this->num_running_ == 0) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void play_complex(const Ts &...x) override {
|
||||
|
||||
@@ -10,166 +10,105 @@ StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> ControllerRegistry::controll
|
||||
|
||||
void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); }
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void ControllerRegistry::notify_binary_sensor_update(binary_sensor::BinarySensor *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_binary_sensor_update(obj);
|
||||
// Macro for standard registry notification dispatch - calls on_<entity_name>_update()
|
||||
#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \
|
||||
void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
for (auto *controller : controllers) { \
|
||||
controller->on_##entity_name##_update(obj); \
|
||||
} \
|
||||
}
|
||||
}
|
||||
|
||||
// Macro for entities where controller method has no "_update" suffix (Event, Update)
|
||||
#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \
|
||||
void ControllerRegistry::notify_##entity_name(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
for (auto *controller : controllers) { \
|
||||
controller->on_##entity_name(obj); \
|
||||
} \
|
||||
}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor)
|
||||
#endif
|
||||
|
||||
#ifdef USE_FAN
|
||||
void ControllerRegistry::notify_fan_update(fan::Fan *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_fan_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(fan::Fan, fan)
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
void ControllerRegistry::notify_light_update(light::LightState *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_light_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(light::LightState, light)
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void ControllerRegistry::notify_sensor_update(sensor::Sensor *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_sensor_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(sensor::Sensor, sensor)
|
||||
#endif
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
void ControllerRegistry::notify_switch_update(switch_::Switch *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_switch_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(switch_::Switch, switch)
|
||||
#endif
|
||||
|
||||
#ifdef USE_COVER
|
||||
void ControllerRegistry::notify_cover_update(cover::Cover *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_cover_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(cover::Cover, cover)
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void ControllerRegistry::notify_text_sensor_update(text_sensor::TextSensor *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_text_sensor_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(text_sensor::TextSensor, text_sensor)
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
void ControllerRegistry::notify_climate_update(climate::Climate *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_climate_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(climate::Climate, climate)
|
||||
#endif
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
void ControllerRegistry::notify_number_update(number::Number *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_number_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(number::Number, number)
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void ControllerRegistry::notify_date_update(datetime::DateEntity *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_date_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(datetime::DateEntity, date)
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void ControllerRegistry::notify_time_update(datetime::TimeEntity *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_time_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(datetime::TimeEntity, time)
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void ControllerRegistry::notify_datetime_update(datetime::DateTimeEntity *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_datetime_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(datetime::DateTimeEntity, datetime)
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
void ControllerRegistry::notify_text_update(text::Text *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_text_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(text::Text, text)
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
void ControllerRegistry::notify_select_update(select::Select *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_select_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(select::Select, select)
|
||||
#endif
|
||||
|
||||
#ifdef USE_LOCK
|
||||
void ControllerRegistry::notify_lock_update(lock::Lock *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_lock_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(lock::Lock, lock)
|
||||
#endif
|
||||
|
||||
#ifdef USE_VALVE
|
||||
void ControllerRegistry::notify_valve_update(valve::Valve *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_valve_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(valve::Valve, valve)
|
||||
#endif
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void ControllerRegistry::notify_media_player_update(media_player::MediaPlayer *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_media_player_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player)
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
void ControllerRegistry::notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_alarm_control_panel_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void ControllerRegistry::notify_event(event::Event *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_event(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event)
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
void ControllerRegistry::notify_update(update::UpdateEntity *obj) {
|
||||
for (auto *controller : controllers) {
|
||||
controller->on_update(obj);
|
||||
}
|
||||
}
|
||||
CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(update::UpdateEntity, update)
|
||||
#endif
|
||||
|
||||
#undef CONTROLLER_REGISTRY_NOTIFY
|
||||
#undef CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_CONTROLLER_REGISTRY
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#define USE_DATETIME_DATE
|
||||
#define USE_DATETIME_DATETIME
|
||||
#define USE_DATETIME_TIME
|
||||
#define USE_DEBUG
|
||||
#define USE_DEEP_SLEEP
|
||||
#define USE_DEVICES
|
||||
#define USE_DISPLAY
|
||||
@@ -143,6 +144,7 @@
|
||||
#define USE_TIME_TIMEZONE
|
||||
#define USE_WIFI
|
||||
#define USE_WIFI_AP
|
||||
#define USE_WIFI_MANUAL_IP
|
||||
#define USE_WIREGUARD
|
||||
#endif
|
||||
|
||||
@@ -286,6 +288,8 @@
|
||||
|
||||
#ifdef USE_NRF52
|
||||
#define USE_NRF52_DFU
|
||||
#define USE_NRF52_REG0_VOUT 5
|
||||
#define USE_NRF52_UICR_ERASE
|
||||
#define USE_SOFTDEVICE_ID 7
|
||||
#define USE_SOFTDEVICE_VERSION 1
|
||||
#endif
|
||||
|
||||
@@ -129,9 +129,6 @@ class EntityBase {
|
||||
// Returns empty StringRef if object_id is dynamic (needs allocation)
|
||||
StringRef get_object_id_ref_for_api_() const;
|
||||
|
||||
/// The hash_base() function has been deprecated. It is kept in this
|
||||
/// class for now, to prevent external components from not compiling.
|
||||
virtual uint32_t hash_base() { return 0L; }
|
||||
void calc_object_id_();
|
||||
|
||||
/// Check if the object_id is dynamic (changes with MAC suffix)
|
||||
|
||||
@@ -1174,12 +1174,18 @@ template<class T> using ExternalRAMAllocator = RAMAllocator<T>;
|
||||
* Functions to constrain the range of arithmetic values.
|
||||
*/
|
||||
|
||||
template<std::totally_ordered T> T clamp_at_least(T value, T min) {
|
||||
template<typename T, typename U>
|
||||
concept comparable_with = requires(T a, U b) {
|
||||
{ a > b } -> std::convertible_to<bool>;
|
||||
{ a < b } -> std::convertible_to<bool>;
|
||||
};
|
||||
|
||||
template<std::totally_ordered T, comparable_with<T> U> T clamp_at_least(T value, U min) {
|
||||
if (value < min)
|
||||
return min;
|
||||
return value;
|
||||
}
|
||||
template<std::totally_ordered T> T clamp_at_most(T value, T max) {
|
||||
template<std::totally_ordered T, comparable_with<T> U> T clamp_at_most(T value, U max) {
|
||||
if (value > max)
|
||||
return max;
|
||||
return value;
|
||||
|
||||
@@ -6,3 +6,7 @@
|
||||
#ifdef USE_ARDUINO
|
||||
#include <Arduino.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
# Lazy Callback Allocation - Tradeoff Analysis
|
||||
|
||||
## Current Implementation
|
||||
```cpp
|
||||
class Sensor {
|
||||
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; // lazy
|
||||
CallbackManager<void(float)> callback_; // always allocated
|
||||
};
|
||||
```
|
||||
|
||||
## Proposed Implementation
|
||||
```cpp
|
||||
class Sensor {
|
||||
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; // lazy
|
||||
std::unique_ptr<CallbackManager<void(float)>> callback_; // ALSO lazy
|
||||
};
|
||||
```
|
||||
|
||||
## Memory Impact (ESP32 32-bit)
|
||||
|
||||
### No Callbacks Registered
|
||||
**Current:**
|
||||
- `raw_callback_` unique_ptr: 4 bytes (nullptr)
|
||||
- `callback_` vector struct: 12 bytes (empty, no heap allocation)
|
||||
- **Total: 16 bytes**
|
||||
|
||||
**Lazy:**
|
||||
- `raw_callback_` unique_ptr: 4 bytes (nullptr)
|
||||
- `callback_` unique_ptr: 4 bytes (nullptr)
|
||||
- **Total: 8 bytes**
|
||||
|
||||
**Savings: 8 bytes per entity without callbacks** ✅
|
||||
|
||||
### One Callback Registered (e.g., MQTT)
|
||||
**Current:**
|
||||
- In object: 4 bytes (raw ptr) + 12 bytes (vector struct) = 16 bytes
|
||||
- On heap: vector allocates storage for std::function ≈ 16 bytes
|
||||
- **Total: 16 + 16 = 32 bytes**
|
||||
|
||||
**Lazy:**
|
||||
- In object: 4 bytes (raw ptr) + 4 bytes (callback ptr) = 8 bytes
|
||||
- Heap #1: CallbackManager object (vector struct) = 12 bytes
|
||||
- Heap #2: vector allocates storage for std::function ≈ 16 bytes
|
||||
- **Total: 8 + 12 + 16 = 36 bytes**
|
||||
|
||||
**Cost: 4 bytes MORE when callbacks are used** ❌
|
||||
|
||||
## Code Changes Required
|
||||
|
||||
### 1. Update publish_state() - Hot path!
|
||||
```cpp
|
||||
// Current
|
||||
void Sensor::internal_send_state_to_frontend(float state) {
|
||||
this->callback_.call(state); // Always valid
|
||||
#ifdef USE_CONTROLLER_REGISTRY
|
||||
ControllerRegistry::notify_sensor_update(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Lazy - adds nullptr check in hot path
|
||||
void Sensor::internal_send_state_to_frontend(float state) {
|
||||
if (this->callback_) { // ← NEW: nullptr check
|
||||
this->callback_->call(state);
|
||||
}
|
||||
#ifdef USE_CONTROLLER_REGISTRY
|
||||
ControllerRegistry::notify_sensor_update(this);
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Update add_on_state_callback()
|
||||
```cpp
|
||||
// Current
|
||||
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
this->callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
// Lazy - lazy allocate on first use
|
||||
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
if (!this->callback_) {
|
||||
this->callback_ = std::make_unique<CallbackManager<void(float)>>();
|
||||
}
|
||||
this->callback_->add(std::move(callback));
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Apply to ALL entity types
|
||||
Need to update:
|
||||
- Sensor, BinarySensor, TextSensor
|
||||
- Climate, Fan, Light, Cover
|
||||
- Switch, Lock, Valve
|
||||
- Number, Select, Text, Button
|
||||
- AlarmControlPanel, MediaPlayer
|
||||
- etc.
|
||||
|
||||
## Performance Impact
|
||||
|
||||
**Hot path (publish_state):** Adds one nullptr check per state update
|
||||
- Branch predictor should handle this well (mostly predictable per entity)
|
||||
- Cost: 1-2 CPU cycles (likely free with branch prediction)
|
||||
|
||||
**Cold path (add callback):** Extra allocation + initialization
|
||||
- Only happens during setup(), not during loop()
|
||||
- Negligible impact
|
||||
|
||||
## Who Benefits?
|
||||
|
||||
### Entities WITHOUT callbacks (saves 8 bytes each):
|
||||
✅ Sensors with **only** API/WebServer (no MQTT, no automations, no copy, no derivatives)
|
||||
✅ Switches with **only** API/WebServer
|
||||
✅ Binary sensors with **only** API/WebServer
|
||||
✅ Covers, Fans, Lights, etc. with **only** API/WebServer
|
||||
|
||||
### Entities WITH callbacks (costs 4 bytes each):
|
||||
❌ Any entity with MQTT enabled
|
||||
❌ Any entity with automations (`on_value:`, `on_state:`)
|
||||
❌ Copy components
|
||||
❌ Derivative sensors (total_daily_energy, integration, etc.)
|
||||
❌ Climate/covers with feedback sensors
|
||||
|
||||
## Realistic Scenario Analysis
|
||||
|
||||
### Scenario 1: Simple API-only device (10 sensors, no MQTT)
|
||||
**Current:** 10 × 16 = 160 bytes
|
||||
**Lazy:** 10 × 8 = 80 bytes
|
||||
**Savings: 80 bytes** ✅
|
||||
|
||||
### Scenario 2: MQTT-enabled device (10 sensors with MQTT)
|
||||
**Current:** 10 × 32 = 320 bytes
|
||||
**Lazy:** 10 × 36 = 360 bytes
|
||||
**Cost: 40 bytes** ❌
|
||||
|
||||
### Scenario 3: Mixed device (5 API-only, 5 with MQTT)
|
||||
**Current:** (5 × 16) + (5 × 32) = 80 + 160 = 240 bytes
|
||||
**Lazy:** (5 × 8) + (5 × 36) = 40 + 180 = 220 bytes
|
||||
**Savings: 20 bytes** ✅
|
||||
|
||||
### Scenario 4: Heavy automation device (10 sensors, MQTT + automation on each)
|
||||
**Current:** 10 × (32 + 16) = 480 bytes (2 callbacks each)
|
||||
**Lazy:** 10 × (36 + 16) = 520 bytes
|
||||
**Cost: 40 bytes** ❌
|
||||
|
||||
## Recommendation
|
||||
|
||||
### Pros:
|
||||
✅ Saves 8 bytes per entity without callbacks
|
||||
✅ Common case: many devices use only API/WebServer after Controller Registry
|
||||
✅ Minimal code complexity (just nullptr checks)
|
||||
✅ Negligible performance impact (predictable branch)
|
||||
✅ Follows existing pattern (raw_callback_ is already lazy)
|
||||
|
||||
### Cons:
|
||||
❌ Costs 4 extra bytes when callbacks ARE used (extra heap allocation)
|
||||
❌ Adds nullptr check to hot path (publish_state called frequently)
|
||||
❌ Requires changes to ALL entity base classes (~15+ files)
|
||||
❌ Users with MQTT enabled pay the 4-byte cost
|
||||
|
||||
### Decision Matrix:
|
||||
**Adopt if:** Most users have API-only devices (no MQTT)
|
||||
**Skip if:** Most users enable MQTT or use many automations
|
||||
|
||||
### Data Needed:
|
||||
- What % of ESPHome devices use MQTT?
|
||||
- What % of entities have automations?
|
||||
- Average entity count per device?
|
||||
|
||||
### My Recommendation: **WORTH CONSIDERING**
|
||||
|
||||
The savings for API-only devices are real (8 bytes per entity), and with Controller Registry, more devices are API-only. The 4-byte cost for MQTT users is small compared to MQTT's overall overhead (~60+ bytes of config per entity).
|
||||
|
||||
**Suggested approach:**
|
||||
1. Start with Sensor (most common entity type)
|
||||
2. Measure real-world impact
|
||||
3. Expand to other entity types if beneficial
|
||||
|
||||
**Code pattern:**
|
||||
```cpp
|
||||
// Helper macro to reduce boilerplate
|
||||
#define LAZY_CALLBACK_CALL(callback_ptr, ...) \
|
||||
do { if (callback_ptr) { callback_ptr->call(__VA_ARGS__); } } while(0)
|
||||
|
||||
void Sensor::internal_send_state_to_frontend(float state) {
|
||||
LAZY_CALLBACK_CALL(this->callback_, state);
|
||||
#ifdef USE_CONTROLLER_REGISTRY
|
||||
ControllerRegistry::notify_sensor_update(this);
|
||||
#endif
|
||||
}
|
||||
```
|
||||
@@ -1,286 +0,0 @@
|
||||
# Partitioned Callback Vector - Final Proposal
|
||||
|
||||
## Design
|
||||
|
||||
Use a **single vector** partitioned into filtered and raw sections, managed with **swap** to maintain O(1) insertion:
|
||||
|
||||
```cpp
|
||||
// Layout: [filtered_0, ..., filtered_n-1, raw_0, ..., raw_m-1]
|
||||
// ^ ^
|
||||
// 0 filtered_count_
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### Header (sensor.h)
|
||||
```cpp
|
||||
class Sensor : public EntityBase, /* ... */ {
|
||||
public:
|
||||
void add_on_state_callback(std::function<void(float)> &&callback);
|
||||
void add_on_raw_state_callback(std::function<void(float)> &&callback);
|
||||
void internal_send_state_to_frontend(float state);
|
||||
void publish_state(float state);
|
||||
|
||||
protected:
|
||||
struct Callbacks {
|
||||
std::vector<std::function<void(float)>> callbacks_; // 12 bytes
|
||||
uint8_t filtered_count_{0}; // 1 byte (+ 3 padding)
|
||||
// Total: 16 bytes on ESP32
|
||||
|
||||
void add_filtered(std::function<void(float)> &&fn) {
|
||||
callbacks_.push_back(std::move(fn));
|
||||
if (filtered_count_ < callbacks_.size() - 1) {
|
||||
// Swap new callback into filtered section
|
||||
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));
|
||||
}
|
||||
|
||||
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_; // 4 bytes, lazy allocated
|
||||
};
|
||||
```
|
||||
|
||||
### Implementation (sensor.cpp)
|
||||
```cpp
|
||||
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
if (!this->callbacks_) {
|
||||
this->callbacks_ = std::make_unique<Callbacks>();
|
||||
}
|
||||
this->callbacks_->add_filtered(std::move(callback));
|
||||
}
|
||||
|
||||
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
|
||||
if (!this->callbacks_) {
|
||||
this->callbacks_ = std::make_unique<Callbacks>();
|
||||
}
|
||||
this->callbacks_->add_raw(std::move(callback));
|
||||
}
|
||||
|
||||
void Sensor::publish_state(float state) {
|
||||
this->raw_state = state;
|
||||
|
||||
// Call raw callbacks (before filters)
|
||||
if (this->callbacks_) {
|
||||
this->callbacks_->call_raw(state);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
|
||||
|
||||
// ... filter logic ...
|
||||
}
|
||||
|
||||
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_) {
|
||||
this->callbacks_->call_filtered(state);
|
||||
}
|
||||
|
||||
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_sensor_update(this);
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
## Memory Comparison (ESP32 32-bit)
|
||||
|
||||
### Current Implementation
|
||||
```cpp
|
||||
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; // 4 bytes
|
||||
CallbackManager<void(float)> callback_; // 12 bytes
|
||||
```
|
||||
|
||||
| Scenario | Current | Partitioned | Savings |
|
||||
|----------|---------|-------------|---------|
|
||||
| No callbacks | 16 bytes | 4 bytes | **12 bytes ✅** |
|
||||
| 1 filtered (MQTT) | 32 bytes | 33 bytes | -1 byte ⚠️ |
|
||||
| 2 filtered | 48 bytes | 49 bytes | -1 byte ⚠️ |
|
||||
| 1 raw + 1 filtered | 60 bytes | 49 bytes | **11 bytes ✅** |
|
||||
| 2 raw + 2 filtered | 92 bytes | 65 bytes | **27 bytes ✅** |
|
||||
|
||||
Wait, let me recalculate this more carefully...
|
||||
|
||||
### Corrected Memory Analysis
|
||||
|
||||
**Current:**
|
||||
- No callbacks: 16 bytes (4 ptr + 12 vec)
|
||||
- 1 filtered: 16 + 16 (fn) = 32 bytes
|
||||
- 2 filtered: 16 + 32 (2 fns) = 48 bytes
|
||||
- 1 raw + 1 filtered: 16 + 12 (raw vec) + 16 (raw fn) + 16 (filtered fn) = 60 bytes
|
||||
|
||||
**Partitioned:**
|
||||
- No callbacks: 4 bytes (nullptr)
|
||||
- 1 filtered: 4 (ptr) + 16 (Callbacks struct) + 16 (fn) = 36 bytes
|
||||
- 2 filtered: 4 + 16 + 32 (2 fns) = 52 bytes
|
||||
- 1 raw + 1 filtered: 4 + 16 + 32 (2 fns) = 52 bytes
|
||||
|
||||
Hmm, the struct is 16 bytes (12 vec + 1 count + 3 padding), so:
|
||||
|
||||
Actually on ESP32:
|
||||
- std::vector = 12 bytes (3 pointers)
|
||||
- uint8_t = 1 byte
|
||||
- padding = 3 bytes (to align to 4)
|
||||
- Total struct: 16 bytes
|
||||
|
||||
But when heap allocated, the struct size is what matters for memory consumption. Let me revise:
|
||||
|
||||
**Partitioned (heap-allocated Callbacks struct):**
|
||||
- Callbacks struct on heap: 12 (vector struct) + 1 (count) + 3 (padding) = 16 bytes
|
||||
- Vector data on heap: N × 16 bytes for N callbacks
|
||||
|
||||
So:
|
||||
- No callbacks: 4 bytes (nullptr) ✅ SAVES 12
|
||||
- 1 filtered: 4 (ptr) + 16 (struct) + 16 (fn) = 36 bytes ⚠️ COSTS 4
|
||||
- 2 filtered: 4 + 16 + 32 = 52 bytes ⚠️ COSTS 4
|
||||
- 1 raw + 1 filtered: 4 + 16 + 32 = 52 bytes ✅ SAVES 8
|
||||
|
||||
Actually wait - in the current implementation, when we have raw + filtered, we have:
|
||||
- 16 bytes base
|
||||
- 12 bytes for raw CallbackManager (heap allocated)
|
||||
- 16 bytes for raw std::function
|
||||
- 16 bytes for filtered std::function
|
||||
= 60 bytes total
|
||||
|
||||
With partitioned:
|
||||
- 4 bytes (ptr)
|
||||
- 16 bytes (Callbacks struct on heap)
|
||||
- 16 bytes (raw fn)
|
||||
- 16 bytes (filtered fn)
|
||||
= 52 bytes total
|
||||
|
||||
SAVES 8 bytes ✅
|
||||
|
||||
Let me make a cleaner table:
|
||||
|
||||
| Scenario | Current | Partitioned | Savings |
|
||||
|----------|---------|-------------|---------|
|
||||
| No callbacks | 16 | 4 | **+12 ✅** |
|
||||
| 1 filtered only | 32 | 36 | **-4 ⚠️** |
|
||||
| 1 raw only | 44 | 36 | **+8 ✅** |
|
||||
| 1 raw + 1 filtered | 60 | 52 | **+8 ✅** |
|
||||
| 2 filtered only | 48 | 52 | **-4 ⚠️** |
|
||||
| 10 API-only sensors | 160 | 40 | **+120 ✅** |
|
||||
| 10 MQTT sensors | 320 | 360 | **-40 ⚠️** |
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Time Complexity
|
||||
- `add_filtered()`: **O(1)** - append + swap
|
||||
- `add_raw()`: **O(1)** - append
|
||||
- `call_filtered()`: **O(n)** - iterate filtered section
|
||||
- `call_raw()`: **O(m)** - iterate raw section
|
||||
|
||||
### Hot Path (publish_state)
|
||||
**Before:**
|
||||
```cpp
|
||||
if (this->callback_) {
|
||||
this->callback_.call(state); // Direct call
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```cpp
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = 0; i < callbacks_->filtered_count_; i++) {
|
||||
callbacks_->callbacks_[i](state);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Performance impact:**
|
||||
- Adds nullptr check (already present for raw_callback_)
|
||||
- Loop is tight, no branching inside
|
||||
- Better cache locality than separate vectors
|
||||
- Negligible impact for 0-2 callbacks (typical case)
|
||||
|
||||
## Advantages
|
||||
|
||||
1. ✅ **Best memory savings**: 12 bytes per entity without callbacks
|
||||
2. ✅ **O(1) insertion**: Both filtered and raw use append (+ optional swap)
|
||||
3. ✅ **No branching**: Hot path has no `if (type == FILTERED)` checks
|
||||
4. ✅ **Cache friendly**: Callbacks stored contiguously
|
||||
5. ✅ **Simple**: One container instead of two
|
||||
6. ✅ **Minimal overhead**: Only 1 byte (+ padding) for partition count
|
||||
|
||||
## Disadvantages
|
||||
|
||||
1. ⚠️ **Costs 4 bytes** for entities with callbacks (vs current)
|
||||
- But saves 12 bytes for entities WITHOUT callbacks (more common after Controller Registry)
|
||||
|
||||
2. ⚠️ **Swap on filtered insertion**
|
||||
- Only during setup(), not runtime
|
||||
- O(1) operation
|
||||
- Negligible impact
|
||||
|
||||
3. ⚠️ **Order not preserved** within each section
|
||||
- Not a problem - callback order doesn't matter
|
||||
- MQTT and automation callbacks are independent
|
||||
|
||||
## Recommendation
|
||||
|
||||
**IMPLEMENT THIS!**
|
||||
|
||||
The partitioned vector with swap is the optimal design because:
|
||||
- After Controller Registry, most entities have 0 callbacks (12-byte savings)
|
||||
- Entities with callbacks pay only 4 extra bytes
|
||||
- O(1) operations, no performance degradation
|
||||
- Cleaner, simpler code
|
||||
|
||||
**Migration strategy:**
|
||||
1. Implement for Sensor first
|
||||
2. Measure real-world impact on flash/RAM
|
||||
3. Apply to BinarySensor, TextSensor
|
||||
4. Expand to other entity types (Climate, Fan, etc.)
|
||||
|
||||
## Code Reusability
|
||||
|
||||
The `Callbacks` struct can be templated for reuse across entity types:
|
||||
|
||||
```cpp
|
||||
template<typename... Args>
|
||||
struct PartitionedCallbacks {
|
||||
std::vector<std::function<void(Args...)>> callbacks_;
|
||||
uint8_t filtered_count_{0};
|
||||
|
||||
void add_filtered(std::function<void(Args...)> &&fn) { /* ... */ }
|
||||
void add_raw(std::function<void(Args...)> &&fn) { /* ... */ }
|
||||
void call_filtered(Args... args) { /* ... */ }
|
||||
void call_raw(Args... args) { /* ... */ }
|
||||
};
|
||||
|
||||
// Usage in different entity types:
|
||||
class Sensor {
|
||||
std::unique_ptr<PartitionedCallbacks<float>> callbacks_;
|
||||
};
|
||||
|
||||
class TextSensor {
|
||||
std::unique_ptr<PartitionedCallbacks<std::string>> callbacks_;
|
||||
};
|
||||
|
||||
class Climate {
|
||||
std::unique_ptr<PartitionedCallbacks<Climate&>> callbacks_;
|
||||
};
|
||||
```
|
||||
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==5.1.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20251013.0
|
||||
aioesphomeapi==42.6.0
|
||||
aioesphomeapi==42.7.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.16 # dashboard_import
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
pylint==4.0.2
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.14.4 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
# Unit tests
|
||||
pytest==8.4.2
|
||||
pytest==9.0.0
|
||||
pytest-cov==7.0.0
|
||||
pytest-mock==3.15.1
|
||||
pytest-asyncio==1.2.0
|
||||
pytest-asyncio==1.3.0
|
||||
pytest-xdist==3.8.0
|
||||
asyncmock==0.4.2
|
||||
hypothesis==6.92.1
|
||||
|
||||
@@ -86,6 +86,7 @@ ISOLATED_COMPONENTS = {
|
||||
"modbus_controller": "Defines multiple modbus buses for testing client/server functionality - conflicts with package modbus bus",
|
||||
"neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)",
|
||||
"packages": "cannot merge packages",
|
||||
"tinyusb": "Conflicts with usb_host component - cannot be used together",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -178,6 +178,14 @@ api:
|
||||
- logger.log: "Skipped loops"
|
||||
- logger.log: "After combined test"
|
||||
|
||||
event:
|
||||
- platform: template
|
||||
name: Test Event
|
||||
id: test_event
|
||||
event_types:
|
||||
- single_click
|
||||
- double_click
|
||||
|
||||
globals:
|
||||
- id: api_continuation_test_counter
|
||||
type: int
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
@@ -52,3 +52,25 @@ sensor:
|
||||
name: "BLE Sensor without Lambda"
|
||||
service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678"
|
||||
characteristic_uuid: "abcd1237-abcd-1234-abcd-abcd12345678"
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
name: "Test Number"
|
||||
id: test_number
|
||||
optimistic: true
|
||||
min_value: 0
|
||||
max_value: 255
|
||||
step: 1
|
||||
|
||||
button:
|
||||
# Test ble_write with lambda that references a component (function pointer)
|
||||
- platform: template
|
||||
name: "BLE Write Lambda Test"
|
||||
on_press:
|
||||
- ble_client.ble_write:
|
||||
id: test_blec
|
||||
service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678"
|
||||
characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678"
|
||||
value: !lambda |-
|
||||
uint8_t val = (uint8_t)id(test_number).state;
|
||||
return std::vector<uint8_t>{0xAA, val, 0xBB};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -37,6 +37,15 @@ canbus:
|
||||
break;
|
||||
}
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
name: "Test Number"
|
||||
id: test_number
|
||||
optimistic: true
|
||||
min_value: 0
|
||||
max_value: 255
|
||||
step: 1
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Canbus Actions
|
||||
@@ -44,3 +53,7 @@ button:
|
||||
- canbus.send: "abc"
|
||||
- canbus.send: [0, 1, 2]
|
||||
- canbus.send: !lambda return {0, 1, 2};
|
||||
# Test canbus.send with lambda that references a component (function pointer)
|
||||
- canbus.send: !lambda |-
|
||||
uint8_t val = (uint8_t)id(test_number).state;
|
||||
return std::vector<uint8_t>{0xAA, val, 0xBB};
|
||||
|
||||
@@ -16,5 +16,6 @@ display:
|
||||
|
||||
touchscreen:
|
||||
- platform: chsc6x
|
||||
i2c_id: i2c_bus
|
||||
display: ili9xxx_display
|
||||
interrupt_pin: 22
|
||||
|
||||
4
tests/components/ds1307/test.nrf52-adafruit.yaml
Normal file
4
tests/components/ds1307/test.nrf52-adafruit.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
41
tests/components/hlk_fm22x/common.yaml
Normal file
41
tests/components/hlk_fm22x/common.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- hlk_fm22x.enroll:
|
||||
name: "Test"
|
||||
direction: 1
|
||||
- hlk_fm22x.delete_all:
|
||||
|
||||
hlk_fm22x:
|
||||
on_face_scan_matched:
|
||||
- logger.log: test_hlk_22x_face_scan_matched
|
||||
on_face_scan_unmatched:
|
||||
- logger.log: test_hlk_22x_face_scan_unmatched
|
||||
on_face_scan_invalid:
|
||||
- logger.log: test_hlk_22x_face_scan_invalid
|
||||
on_face_info:
|
||||
- logger.log: test_hlk_22x_face_info
|
||||
on_enrollment_done:
|
||||
- logger.log: test_hlk_22x_enrollment_done
|
||||
on_enrollment_failed:
|
||||
- logger.log: test_hlk_22x_enrollment_failed
|
||||
|
||||
sensor:
|
||||
- platform: hlk_fm22x
|
||||
face_count:
|
||||
name: "Face Count"
|
||||
last_face_id:
|
||||
name: "Last Face ID"
|
||||
status:
|
||||
name: "Face Status"
|
||||
|
||||
binary_sensor:
|
||||
- platform: hlk_fm22x
|
||||
name: "Face Enrolling"
|
||||
|
||||
text_sensor:
|
||||
- platform: hlk_fm22x
|
||||
version:
|
||||
name: "HLK Version"
|
||||
last_face_name:
|
||||
name: "Last Face Name"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user