1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-12 21:05:46 +00:00

Compare commits

..

71 Commits

Author SHA1 Message Date
J. Nick Koston
4eb471b316 tweaks 2025-11-12 15:01:05 -06:00
J. Nick Koston
20388ce848 [thermostat] Replace std::map with FixedVector, reduce flash usage 2025-11-12 14:54:23 -06:00
J. Nick Koston
9de80b635a [core] Fix wait_until hanging when used in on_boot automations (#11869) 2025-11-12 17:56:19 +00:00
Jonathan Swoboda
748aee584a [esp32] Update the recommended platform to 55.03.31-2 (#11865) 2025-11-12 10:41:22 -05:00
Jonathan Swoboda
3cbfddcc83 Merge branch 'beta' into dev 2025-11-11 23:27:24 -05:00
Jonathan Swoboda
1d71b6b93e Merge pull request #11862 from esphome/bump-2025.11.0b1
2025.11.0b1
2025-11-11 23:27:12 -05:00
J. Nick Koston
398dba4fc8 [ci] Reduce release time by removing 21 redundant ESP32-S3 IDF tests (#11850) 2025-11-12 16:44:19 +13:00
Jonathan Swoboda
298813d4fa Bump version to 2025.11.0b1 2025-11-11 22:14:22 -05:00
Jonathan Swoboda
56d141c741 Merge branch 'release' into dev 2025-11-11 20:09:55 -05:00
Jonathan Swoboda
47a7f729dd Merge pull request #11857 from esphome/bump-2025.10.5
2025.10.5
2025-11-11 20:09:41 -05:00
Jonathan Swoboda
7806eb980f Bump version to 2025.12.0-dev 2025-11-11 19:50:47 -05:00
Jonathan Swoboda
a59888224c Bump version to 2025.10.5 2025-11-11 19:44:37 -05:00
Clyde Stubbs
58ad4759f0 [lvgl] Fix rotation with unusual width (#11680) 2025-11-11 19:44:37 -05:00
Clyde Stubbs
87f79290ba [usb_uart] Fixes for transfer queue allocation (#11548) 2025-11-11 19:44:37 -05:00
Jonathan Swoboda
9326d78439 [core] Don't allow python 3.14 (#11527) 2025-11-11 19:44:37 -05:00
Stuart Parmenter
a93887a790 [const] Add CONF_ROWS (#11249) 2025-11-11 19:44:37 -05:00
Kevin Ahrendt
d7fa131a8a [network, psram, speaker wifi] Use CORE.data to enable high performance networking (#11812) 2025-11-11 18:43:06 -06:00
J. Nick Koston
79a4444928 [wifi] Conditionally compile manual_ip to save 24-72 bytes RAM (#11833) 2025-11-11 23:27:08 +00:00
J. Nick Koston
572fae5c7d [wifi] Restore two-attempt BSSID filtering for mesh networks (#11844) 2025-11-12 12:12:53 +13:00
J. Nick Koston
5dafaaced4 [wifi] Fix scan and connection failures after adapter restart (#11851)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-11 23:12:10 +00:00
J. Nick Koston
65a303d48f [wifi] Add min_auth_mode configuration option (#11814) 2025-11-11 16:39:55 -06:00
J. Nick Koston
00c71b7236 [wifi] Fix all-hidden networks duplicate attempts and scan skipping (#11848)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-11 22:33:37 +00:00
J. Nick Koston
ef04903a7a [wifi] Change priority type from float to int8_t (#11830) 2025-11-12 11:10:17 +13:00
J. Nick Koston
a2ec7f622c [wifi] Fix infinite retry loop when no hidden networks and captive portal active (#11831) 2025-11-11 16:04:37 -06:00
tomaszduda23
2f91e7bd47 [nrf52] fix boot loop (#11854) 2025-11-11 15:33:53 -06:00
tomaszduda23
80a7c6d3c3 [nrf52,debug] add partition dump (#11839)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-11 14:52:41 -06:00
CzBiX
7a92565a0c [lvgl] Fix compile when using transform_zoom (#11845) 2025-11-12 06:24:52 +11:00
tomaszduda23
661920c51e [nrf52,ssd1306_i2c] fix build error (#11847) 2025-11-11 18:18:17 +00:00
tomaszduda23
a6b905e148 [nrf52,pcf8563] fix build error (#11846)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-11 17:50:07 +00:00
tomaszduda23
a6b7c1f18c [nrf52,gpio] add gpio levels for high voltage mode (#9858)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-11 15:17:25 +00:00
Clyde Stubbs
7a700ca077 [core] Update clamp functions to allow mixed but comparable types (#11828) 2025-11-11 02:15:44 +00:00
Clyde Stubbs
1539b43074 [wifi][ethernet] Don't block setup until connected (#9823)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-11-10 19:17:16 -06:00
Jesse Hills
463a00b1ac [CI] Don't request codeowners review in forks (#11827) 2025-11-10 19:10:29 -06:00
J. Nick Koston
82692d7053 [tests] Migrate components to shared packages and fix ID ambiguity (#11819) 2025-11-10 19:00:54 -06:00
J. Nick Koston
1cccfdd2b9 [wifi] Fix mesh network failover and improve retry logic reliability (#11805) 2025-11-11 13:40:23 +13:00
Beormund
855aa32f54 Add support for RX8130 RTC Chip (#10511)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-10 19:32:59 -05:00
Stuart Parmenter
0f8332fe3c [lvgl] Automatically register widget types (#11394)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-11-11 11:04:03 +11:00
Thomas Rupprecht
40e2976ba2 [ai] simplify namespace syntax (#11824) 2025-11-10 17:33:34 -06:00
dependabot[bot]
e46300828e Bump pytest from 8.4.2 to 9.0.0 (#11817)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 13:45:56 -06:00
dependabot[bot]
8c5b964722 Bump pyupgrade from 3.21.0 to 3.21.1 (#11816)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 13:28:25 -06:00
dependabot[bot]
43eafbccb3 Bump pytest-asyncio from 1.2.0 to 1.3.0 (#11815)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 13:28:14 -06:00
J. Nick Koston
f32b69b8f1 [tests] Add unit test coverage for web_port property (#11811) 2025-11-10 10:00:42 -06:00
On Freund
2a16653642 HLK-FM22X Face Recognition module component (#8059)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-11-10 07:44:27 -06:00
tomaszduda23
b47e89a7d5 [nrf52,watchdog] do not disable watchog if it is not nesesery (#11686) 2025-11-10 15:21:38 +13:00
J. Nick Koston
c17a31a8f8 Ensure event paths are enabled in api compile tests (#11776) 2025-11-10 14:28:49 +13:00
Paul Schulz
fbbdad75f6 [sx126x] Change BUSY, RST, DIO1 pins to general GPIO (from internal) (#11782) 2025-11-10 14:26:02 +13:00
J. Nick Koston
7abb6d4998 [core] Implement Global Controller Registry to reduce RAM usage (#11772) 2025-11-09 17:34:08 -06:00
Ludovic BOUÉ
1dabe83d04 [nrf52] api (#11751) 2025-11-10 11:48:33 +13:00
J. Nick Koston
0d735dc259 [remote_base] Optimize abbwelcome action memory usage - store static data in flash (#11798)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-09 22:46:01 +00:00
J. Nick Koston
7b86e1feb0 [core] Remove deprecated EntityBase::hash_base() method (#11783) 2025-11-10 11:39:27 +13:00
J. Nick Koston
d516627957 [uart] Store static data in flash and use function pointers for lambdas (#11784)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-09 22:37:14 +00:00
J. Nick Koston
fb1c67490a [udp] Optimize udp.write action memory usage - store static data in flash (#11794)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-09 22:33:56 +00:00
J. Nick Koston
8b9600b930 [speaker] Optimize speaker.play action memory usage - store static data in flash (#11796)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-09 22:33:29 +00:00
J. Nick Koston
cbb98c4050 [bl0940] Fix calibration number preference hash for multi-device configs (#11769) 2025-11-10 11:27:56 +13:00
J. Nick Koston
e7ff56f1cd [remote_base] Eliminate substr() allocations in Pronto dump logging (#11726) 2025-11-10 11:27:09 +13:00
J. Nick Koston
7705a5de06 [sx127x] Optimize send_packet action memory usage - store static data in flash (#11792)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-09 22:25:40 +00:00
J. Nick Koston
77ab096b59 [remote_base] Optimize raw transmit action memory usage - use function pointers (#11800) 2025-11-10 11:25:16 +13:00
J. Nick Koston
26a3ec41d6 [sx126x] Optimize send_packet action memory usage - store static data in flash (#11790)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-09 22:23:33 +00:00
J. Nick Koston
3bcbfe8d97 [canbus] Optimize canbus.send memory usage - store static data in flash (#11788)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-09 22:22:15 +00:00
J. Nick Koston
870b2c4f84 [ble_client] Optimize ble_write memory usage - store static data in flash (#11786)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-10 11:21:25 +13:00
J. Nick Koston
5f9c7a70ff Add additional tests for remote_transmitter raw (#11801) 2025-11-10 11:17:14 +13:00
J. Nick Koston
f7179d4255 Add additonal abbwelcome remote_base tests (#11799) 2025-11-10 11:16:53 +13:00
J. Nick Koston
eb0558ca3f Add additional udp lambda tests (#11795) 2025-11-10 11:16:09 +13:00
J. Nick Koston
5585355263 Add additional speaker lambda tests (#11797) 2025-11-10 11:15:50 +13:00
J. Nick Koston
e468ca4881 Add additional sx127x lambda tests (#11793) 2025-11-10 11:11:31 +13:00
J. Nick Koston
4c078dea2c Add additional sx126x lambda tests (#11791) 2025-11-10 11:10:31 +13:00
J. Nick Koston
783dbd1e6b Add additional compile time tests for canbus (#11789) 2025-11-10 11:09:46 +13:00
J. Nick Koston
b49619d9bf Add ble_client lambda compile tests (#11787) 2025-11-10 11:09:25 +13:00
J. Nick Koston
a290b88cd6 Expand uart.write tests (#11785) 2025-11-10 11:09:03 +13:00
dependabot[bot]
b61027607f Bump aioesphomeapi from 42.6.0 to 42.7.0 (#11771)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-08 15:22:40 -06:00
optimusprimespace
f55c872180 Updated AQI calculation for HM3301 to the new standard (#9442)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-08 14:56:51 -06:00
158 changed files with 3680 additions and 3454 deletions

View File

@@ -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:**

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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_;

View File

@@ -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

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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() {

View File

@@ -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};

View File

@@ -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) {

View File

@@ -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; }

View File

@@ -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"),
}

View File

@@ -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());

View File

@@ -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;

View File

@@ -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();

View 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

View 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))

View 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

View 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

View 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))

View 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))

View File

@@ -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);

View File

@@ -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; }

View File

@@ -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());

View File

@@ -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,

View File

@@ -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
"""

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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:

View File

@@ -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):

View 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

View File

@@ -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; }

View File

@@ -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; }

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

View 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

View 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

View 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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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> {

View File

@@ -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)

View File

@@ -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

View File

@@ -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> {

View File

@@ -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};

View File

@@ -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

View File

@@ -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> {

View File

@@ -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]

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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};

View File

@@ -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;
}

View File

@@ -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, &current_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;
}

View File

@@ -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();

View File

@@ -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));

View File

@@ -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);
}
}
}

View File

@@ -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 = (

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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;

View File

@@ -6,3 +6,7 @@
#ifdef USE_ARDUINO
#include <Arduino.h>
#endif
#ifdef USE_ZEPHYR
#define M_PI 3.14159265358979323846
#endif

View File

@@ -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
}
```

View File

@@ -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_;
};
```

View File

@@ -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

View File

@@ -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

View File

@@ -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",
}

View File

@@ -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

View File

@@ -1,2 +0,0 @@
packages:
common: !include common.yaml

View File

@@ -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};

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -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};

View File

@@ -16,5 +16,6 @@ display:
touchscreen:
- platform: chsc6x
i2c_id: i2c_bus
display: ili9xxx_display
interrupt_pin: 22

View File

@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
<<: !include common.yaml

View 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