1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-24 04:33:49 +01:00

Merge branch 'dev' into ListEntitiesServicesArgument_FixedVector

This commit is contained in:
J. Nick Koston
2025-10-14 16:25:06 -10:00
committed by GitHub
33 changed files with 278 additions and 108 deletions

View File

@@ -221,6 +221,104 @@ This document provides essential context for AI models interacting with this pro
* **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests. * **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests.
* **Code Generation:** Generate minimal and efficient C++ code. Validate all user inputs thoroughly. Support multiple platform variations. * **Code Generation:** Generate minimal and efficient C++ code. Validate all user inputs thoroughly. Support multiple platform variations.
* **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization. * **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization.
* **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage.
**STL Container Guidelines:**
ESPHome runs on embedded systems with limited resources. Choose containers carefully:
1. **Compile-time-known sizes:** Use `std::array` instead of `std::vector` when size is known at compile time.
```cpp
// Bad - generates STL realloc code
std::vector<int> values;
// Good - no dynamic allocation
std::array<int, MAX_VALUES> values;
```
Use `cg.add_define("MAX_VALUES", count)` to set the size from Python configuration.
**For byte buffers:** Avoid `std::vector<uint8_t>` unless the buffer needs to grow. Use `std::unique_ptr<uint8_t[]>` instead.
> **Note:** `std::unique_ptr<uint8_t[]>` does **not** provide bounds checking or iterator support like `std::vector<uint8_t>`. Use it only when you do not need these features and want minimal overhead.
```cpp
// Bad - STL overhead for simple byte buffer
std::vector<uint8_t> buffer;
buffer.resize(256);
// Good - minimal overhead, single allocation
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(256);
// Or if size is constant:
std::array<uint8_t, 256> buffer;
```
2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for fixed-size stack allocation with `push_back()` interface.
```cpp
// Bad - generates STL realloc code (_M_realloc_insert)
std::vector<ServiceRecord> services;
services.reserve(5); // Still includes reallocation machinery
// Good - compile-time fixed size, stack allocated, no reallocation machinery
StaticVector<ServiceRecord, MAX_SERVICES> services; // Allocates all MAX_SERVICES on stack
services.push_back(record1); // Tracks count but all slots allocated
```
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization.
```cpp
// Bad - generates STL realloc code (_M_realloc_insert)
std::vector<TxtRecord> txt_records;
txt_records.reserve(5); // Still includes reallocation machinery
// Good - runtime size, single allocation, no reallocation machinery
FixedVector<TxtRecord> txt_records;
txt_records.init(record_count); // Initialize with exact size at runtime
```
**Benefits:**
- Eliminates `_M_realloc_insert`, `_M_default_append` template instantiations (saves 200-500 bytes per instance)
- Single allocation, no upper bound needed
- No reallocation overhead
- Compatible with protobuf code generation when using `[(fixed_vector) = true]` option
4. **Small datasets (1-16 elements):** Use `std::vector` or `std::array` with simple structs instead of `std::map`/`std::set`/`std::unordered_map`.
```cpp
// Bad - 2KB+ overhead for red-black tree/hash table
std::map<std::string, int> small_lookup;
std::unordered_map<int, std::string> tiny_map;
// Good - simple struct with linear search (std::vector is fine)
struct LookupEntry {
const char *key;
int value;
};
std::vector<LookupEntry> small_lookup = {
{"key1", 10},
{"key2", 20},
{"key3", 30},
};
// Or std::array if size is compile-time constant:
// std::array<LookupEntry, 3> small_lookup = {{ ... }};
```
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
5. **Detection:** Look for these patterns in compiler output:
- Large code sections with STL symbols (vector, map, set)
- `alloc`, `realloc`, `dealloc` in symbol names
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
- Red-black tree code (`rb_tree`, `_Rb_tree`)
- Hash table infrastructure (`unordered_map`, `hash`)
**When to optimize:**
- Core components (API, network, logger)
- Widely-used components (mdns, wifi, ble)
- Components causing flash size complaints
**When not to optimize:**
- Single-use niche components
- Code where readability matters more than bytes
- Already using appropriate containers
* **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals. * **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.
**Bad Pattern (Module-Level Globals):** **Bad Pattern (Module-Level Globals):**

View File

@@ -776,9 +776,9 @@ message HomeassistantActionRequest {
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES"; option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
string service = 1; string service = 1;
repeated HomeassistantServiceMap data = 2; repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true];
repeated HomeassistantServiceMap data_template = 3; repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true];
repeated HomeassistantServiceMap variables = 4; repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true];
bool is_event = 5; bool is_event = 5;
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"]; uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];

View File

@@ -1110,9 +1110,9 @@ class HomeassistantActionRequest final : public ProtoMessage {
#endif #endif
StringRef service_ref_{}; StringRef service_ref_{};
void set_service(const StringRef &ref) { this->service_ref_ = ref; } void set_service(const StringRef &ref) { this->service_ref_ = ref; }
std::vector<HomeassistantServiceMap> data{}; FixedVector<HomeassistantServiceMap> data{};
std::vector<HomeassistantServiceMap> data_template{}; FixedVector<HomeassistantServiceMap> data_template{};
std::vector<HomeassistantServiceMap> variables{}; FixedVector<HomeassistantServiceMap> variables{};
bool is_event{false}; bool is_event{false};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
uint32_t call_id{0}; uint32_t call_id{0};

View File

@@ -201,9 +201,9 @@ class CustomAPIDevice {
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) { void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
resp.set_service(StringRef(service_name)); resp.set_service(StringRef(service_name));
resp.data.init(data.size());
for (auto &it : data) { for (auto &it : data) {
resp.data.emplace_back(); auto &kv = resp.data.emplace_back();
auto &kv = resp.data.back();
kv.set_key(StringRef(it.first)); kv.set_key(StringRef(it.first));
kv.value = it.second; kv.value = it.second;
} }
@@ -244,9 +244,9 @@ class CustomAPIDevice {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
resp.set_service(StringRef(service_name)); resp.set_service(StringRef(service_name));
resp.is_event = true; resp.is_event = true;
resp.data.init(data.size());
for (auto &it : data) { for (auto &it : data) {
resp.data.emplace_back(); auto &kv = resp.data.emplace_back();
auto &kv = resp.data.back();
kv.set_key(StringRef(it.first)); kv.set_key(StringRef(it.first));
kv.value = it.second; kv.value = it.second;
} }

View File

@@ -127,24 +127,9 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
std::string service_value = this->service_.value(x...); std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value)); resp.set_service(StringRef(service_value));
resp.is_event = this->flags_.is_event; resp.is_event = this->flags_.is_event;
for (auto &it : this->data_) { this->populate_service_map(resp.data, this->data_, x...);
resp.data.emplace_back(); this->populate_service_map(resp.data_template, this->data_template_, x...);
auto &kv = resp.data.back(); this->populate_service_map(resp.variables, this->variables_, x...);
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
for (auto &it : this->data_template_) {
resp.data_template.emplace_back();
auto &kv = resp.data_template.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
for (auto &it : this->variables_) {
resp.variables.emplace_back();
auto &kv = resp.variables.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
if (this->flags_.wants_status) { if (this->flags_.wants_status) {
@@ -189,6 +174,16 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
} }
protected: protected:
template<typename VectorType, typename SourceType>
static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) {
dest.init(source.size());
for (auto &it : source) {
auto &kv = dest.emplace_back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
}
APIServer *parent_; APIServer *parent_;
TemplatableStringValue<Ts...> service_{}; TemplatableStringValue<Ts...> service_{};
std::vector<TemplatableKeyValuePair<Ts...>> data_; std::vector<TemplatableKeyValuePair<Ts...>> data_;

View File

@@ -90,13 +90,12 @@ void HomeassistantNumber::control(float value) {
api::HomeassistantActionRequest resp; api::HomeassistantActionRequest resp;
resp.set_service(SERVICE_NAME); resp.set_service(SERVICE_NAME);
resp.data.emplace_back(); resp.data.init(2);
auto &entity_id = resp.data.back(); auto &entity_id = resp.data.emplace_back();
entity_id.set_key(ENTITY_ID_KEY); entity_id.set_key(ENTITY_ID_KEY);
entity_id.value = this->entity_id_; entity_id.value = this->entity_id_;
resp.data.emplace_back(); auto &entity_value = resp.data.emplace_back();
auto &entity_value = resp.data.back();
entity_value.set_key(VALUE_KEY); entity_value.set_key(VALUE_KEY);
entity_value.value = to_string(value); entity_value.value = to_string(value);

View File

@@ -51,8 +51,8 @@ void HomeassistantSwitch::write_state(bool state) {
resp.set_service(SERVICE_OFF); resp.set_service(SERVICE_OFF);
} }
resp.data.emplace_back(); resp.data.init(1);
auto &entity_id_kv = resp.data.back(); auto &entity_id_kv = resp.data.emplace_back();
entity_id_kv.set_key(ENTITY_ID_KEY); entity_id_kv.set_key(ENTITY_ID_KEY);
entity_id_kv.value = this->entity_id_; entity_id_kv.value = this->entity_id_;

View File

@@ -218,7 +218,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
} }
case improv::GET_WIFI_NETWORKS: { case improv::GET_WIFI_NETWORKS: {
std::vector<std::string> networks; std::vector<std::string> networks;
auto results = wifi::global_wifi_component->get_scan_result(); const auto &results = wifi::global_wifi_component->get_scan_result();
for (auto &scan : results) { for (auto &scan : results) {
if (scan.get_is_hidden()) if (scan.get_is_hidden())
continue; continue;

View File

@@ -177,9 +177,10 @@ void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ =
void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; } void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; }
bool LightState::supports_effects() { return !this->effects_.empty(); } bool LightState::supports_effects() { return !this->effects_.empty(); }
const std::vector<LightEffect *> &LightState::get_effects() const { return this->effects_; } const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
void LightState::add_effects(const std::vector<LightEffect *> &effects) { void LightState::add_effects(const std::vector<LightEffect *> &effects) {
this->effects_.reserve(this->effects_.size() + effects.size()); // Called once from Python codegen during setup with all effects from YAML config
this->effects_.init(effects.size());
for (auto *effect : effects) { for (auto *effect : effects) {
this->effects_.push_back(effect); this->effects_.push_back(effect);
} }

View File

@@ -11,8 +11,9 @@
#include "light_traits.h" #include "light_traits.h"
#include "light_transformer.h" #include "light_transformer.h"
#include <vector> #include "esphome/core/helpers.h"
#include <strings.h> #include <strings.h>
#include <vector>
namespace esphome { namespace esphome {
namespace light { namespace light {
@@ -159,7 +160,7 @@ class LightState : public EntityBase, public Component {
bool supports_effects(); bool supports_effects();
/// Get all effects for this light state. /// Get all effects for this light state.
const std::vector<LightEffect *> &get_effects() const; const FixedVector<LightEffect *> &get_effects() const;
/// Add effects for this light state. /// Add effects for this light state.
void add_effects(const std::vector<LightEffect *> &effects); void add_effects(const std::vector<LightEffect *> &effects);
@@ -260,7 +261,7 @@ class LightState : public EntityBase, public Component {
/// The currently active transformer for this light (transition/flash). /// The currently active transformer for this light (transition/flash).
std::unique_ptr<LightTransformer> transformer_{nullptr}; std::unique_ptr<LightTransformer> transformer_{nullptr};
/// List of effects for this light. /// List of effects for this light.
std::vector<LightEffect *> effects_; FixedVector<LightEffect *> effects_;
/// Object used to store the persisted values of the light. /// Object used to store the persisted values of the light.
ESPPreferenceObject rtc_; ESPPreferenceObject rtc_;
/// Value for storing the index of the currently active effect. 0 if no effect is active /// Value for storing the index of the currently active effect. 0 if no effect is active

View File

@@ -83,7 +83,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
#endif #endif
auto &txt_records = service.txt_records; auto &txt_records = service.txt_records;
txt_records.reserve(txt_count); txt_records.init(txt_count);
if (!friendly_name_empty) { if (!friendly_name_empty) {
txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())}); txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())});
@@ -171,12 +171,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
fallback_service.service_type = MDNS_STR(SERVICE_HTTP); fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
fallback_service.proto = MDNS_STR(SERVICE_TCP); fallback_service.proto = MDNS_STR(SERVICE_TCP);
fallback_service.port = USE_WEBSERVER_PORT; fallback_service.port = USE_WEBSERVER_PORT;
fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}); fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}};
#endif
#ifdef USE_MDNS_STORE_SERVICES
// Copy to member variable if storage is enabled (verbose logging, OpenThread, or extra services)
this->services_ = services;
#endif #endif
} }

View File

@@ -38,7 +38,7 @@ struct MDNSService {
// as defined in RFC6763 Section 7, like "_tcp" or "_udp" // as defined in RFC6763 Section 7, like "_tcp" or "_udp"
const MDNSString *proto; const MDNSString *proto;
TemplatableValue<uint16_t> port; TemplatableValue<uint16_t> port;
std::vector<MDNSTXTRecord> txt_records; FixedVector<MDNSTXTRecord> txt_records;
}; };
class MDNSComponent : public Component { class MDNSComponent : public Component {

View File

@@ -12,8 +12,13 @@ namespace mdns {
static const char *const TAG = "mdns"; static const char *const TAG = "mdns";
void MDNSComponent::setup() { void MDNSComponent::setup() {
#ifdef USE_MDNS_STORE_SERVICES
this->compile_records_(this->services_);
const auto &services = this->services_;
#else
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services; StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
this->compile_records_(services); this->compile_records_(services);
#endif
esp_err_t err = mdns_init(); esp_err_t err = mdns_init();
if (err != ESP_OK) { if (err != ESP_OK) {

View File

@@ -12,8 +12,13 @@ namespace esphome {
namespace mdns { namespace mdns {
void MDNSComponent::setup() { void MDNSComponent::setup() {
#ifdef USE_MDNS_STORE_SERVICES
this->compile_records_(this->services_);
const auto &services = this->services_;
#else
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services; StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
this->compile_records_(services); this->compile_records_(services);
#endif
MDNS.begin(this->hostname_.c_str()); MDNS.begin(this->hostname_.c_str());

View File

@@ -12,8 +12,13 @@ namespace esphome {
namespace mdns { namespace mdns {
void MDNSComponent::setup() { void MDNSComponent::setup() {
#ifdef USE_MDNS_STORE_SERVICES
this->compile_records_(this->services_);
const auto &services = this->services_;
#else
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services; StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
this->compile_records_(services); this->compile_records_(services);
#endif
MDNS.begin(this->hostname_.c_str()); MDNS.begin(this->hostname_.c_str());

View File

@@ -12,8 +12,13 @@ namespace esphome {
namespace mdns { namespace mdns {
void MDNSComponent::setup() { void MDNSComponent::setup() {
#ifdef USE_MDNS_STORE_SERVICES
this->compile_records_(this->services_);
const auto &services = this->services_;
#else
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services; StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
this->compile_records_(services); this->compile_records_(services);
#endif
MDNS.begin(this->hostname_.c_str()); MDNS.begin(this->hostname_.c_str());

View File

@@ -56,50 +56,41 @@ DriverChip(
"WAVESHARE-P4-86-PANEL", "WAVESHARE-P4-86-PANEL",
height=720, height=720,
width=720, width=720,
hsync_back_porch=80, hsync_back_porch=50,
hsync_pulse_width=20, hsync_pulse_width=20,
hsync_front_porch=80, hsync_front_porch=50,
vsync_back_porch=12, vsync_back_porch=20,
vsync_pulse_width=4, vsync_pulse_width=4,
vsync_front_porch=30, vsync_front_porch=20,
pclk_frequency="46MHz", pclk_frequency="38MHz",
lane_bit_rate="1Gbps", lane_bit_rate="480Mbps",
swap_xy=cv.UNDEFINED, swap_xy=cv.UNDEFINED,
color_order="RGB", color_order="RGB",
reset_pin=27, reset_pin=27,
initsequence=[ initsequence=[
(0xB9, 0xF1, 0x12, 0x83), (0xB9, 0xF1, 0x12, 0x83),
( (0xB1, 0x00, 0x00, 0x00, 0xDA, 0x80),
0xBA, 0x31, 0x81, 0x05, 0xF9, 0x0E, 0x0E, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00, (0xB2, 0x3C, 0x12, 0x30),
0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37,
),
(0xB8, 0x25, 0x22, 0xF0, 0x63),
(0xBF, 0x02, 0x11, 0x00),
(0xB3, 0x10, 0x10, 0x28, 0x28, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00), (0xB3, 0x10, 0x10, 0x28, 0x28, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00),
(0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00), (0xB4, 0x80),
(0xBC, 0x46), (0xCC, 0x0B), (0xB4, 0x80), (0xB2, 0x3C, 0x12, 0x30),
(0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x03, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10,),
(0xC1, 0x36, 0x00, 0x32, 0x32, 0x77, 0xF1, 0xCC, 0xCC, 0x77, 0x77, 0x33, 0x33),
(0xB5, 0x0A, 0x0A), (0xB5, 0x0A, 0x0A),
(0xB6, 0xB2, 0xB2), (0xB6, 0x97, 0x97),
( (0xB8, 0x26, 0x22, 0xF0, 0x13),
0xE9, 0xC8, 0x10, 0x0A, 0x10, 0x0F, 0xA1, 0x80, 0x12, 0x31, 0x23, 0x47, 0x86, 0xA1, 0x80, (0xBA, 0x31, 0x81, 0x0F, 0xF9, 0x0E, 0x06, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00, 0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37),
0x47, 0x08, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x48, (0xBC, 0x47),
0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x48, 0x13, 0x8B, 0xAF, 0x57, (0xBF, 0x02, 0x11, 0x00),
0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00),
0x00, 0x00, 0x00, 0x00, (0xC1, 0x25, 0x00, 0x32, 0x32, 0x77, 0xE4, 0xFF, 0xFF, 0xCC, 0xCC, 0x77, 0x77),
), (0xC6, 0x82, 0x00, 0xBF, 0xFF, 0x00, 0xFF),
( (0xC7, 0xB8, 0x00, 0x0A, 0x10, 0x01, 0x09),
0xEA, 0x96, 0x12, 0x01, 0x01, 0x01, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x31, (0xC8, 0x10, 0x40, 0x1E, 0x02),
0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x4F, 0x20, 0x8B, 0xA8, 0x20, 0x64, (0xCC, 0x0B),
0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, (0xE0, 0x00, 0x0B, 0x10, 0x2C, 0x3D, 0x3F, 0x42, 0x3A, 0x07, 0x0D, 0x0F, 0x13, 0x15, 0x13, 0x14, 0x0F, 0x16, 0x00, 0x0B, 0x10, 0x2C, 0x3D, 0x3F, 0x42, 0x3A, 0x07, 0x0D, 0x0F, 0x13, 0x15, 0x13, 0x14, 0x0F, 0x16),
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xA1, 0x80, 0x00, 0x00, (0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x0B, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10),
0x00, 0x00, (0xE9, 0xC8, 0x10, 0x0A, 0x00, 0x00, 0x80, 0x81, 0x12, 0x31, 0x23, 0x4F, 0x86, 0xA0, 0x00, 0x47, 0x08, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x98, 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x98, 0x13, 0x8B, 0xAF, 0x57, 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
), (0xEA, 0x97, 0x0C, 0x09, 0x09, 0x09, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x31, 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x9F, 0x20, 0x8B, 0xA8, 0x20, 0x64, 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x02, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x81, 0x00, 0x00, 0x00, 0x00),
( (0xEF, 0xFF, 0xFF, 0x01),
0xE0, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13, 0x15, 0x14, (0x11, 0x00),
0x15, 0x10, 0x17, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13, (0x29, 0x00),
0x15, 0x14, 0x15, 0x10, 0x17,
),
], ],
) )

View File

@@ -380,24 +380,25 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
if (this->on_connect_) { if (this->on_connect_) {
this->on_connect_(rsp); this->on_connect_(rsp);
} }
this->sessions_.insert(rsp); this->sessions_.push_back(rsp);
} }
void AsyncEventSource::loop() { void AsyncEventSource::loop() {
// Clean up dead sessions safely // Clean up dead sessions safely
// This follows the ESP-IDF pattern where free_ctx marks resources as dead // This follows the ESP-IDF pattern where free_ctx marks resources as dead
// and the main loop handles the actual cleanup to avoid race conditions // and the main loop handles the actual cleanup to avoid race conditions
auto it = this->sessions_.begin(); for (size_t i = 0; i < this->sessions_.size();) {
while (it != this->sessions_.end()) { auto *ses = this->sessions_[i];
auto *ses = *it;
// If the session has a dead socket (marked by destroy callback) // If the session has a dead socket (marked by destroy callback)
if (ses->fd_.load() == 0) { if (ses->fd_.load() == 0) {
ESP_LOGD(TAG, "Removing dead event source session"); ESP_LOGD(TAG, "Removing dead event source session");
it = this->sessions_.erase(it);
delete ses; // NOLINT(cppcoreguidelines-owning-memory) delete ses; // NOLINT(cppcoreguidelines-owning-memory)
// Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
this->sessions_[i] = this->sessions_.back();
this->sessions_.pop_back();
} else { } else {
ses->loop(); ses->loop();
++it; ++i;
} }
} }
} }

View File

@@ -8,7 +8,6 @@
#include <functional> #include <functional>
#include <list> #include <list>
#include <map> #include <map>
#include <set>
#include <string> #include <string>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -315,7 +314,10 @@ class AsyncEventSource : public AsyncWebHandler {
protected: protected:
std::string url_; std::string url_;
std::set<AsyncEventSourceResponse *> sessions_; // Use vector instead of set: SSE sessions are typically 1-5 connections (browsers, dashboards).
// Linear search is faster than red-black tree overhead for this small dataset.
// Only operations needed: add session, remove session, iterate sessions - no need for sorted order.
std::vector<AsyncEventSourceResponse *> sessions_;
connect_handler_t on_connect_{}; connect_handler_t on_connect_{};
esphome::web_server::WebServer *web_server_; esphome::web_server::WebServer *web_server_;
}; };

View File

@@ -447,6 +447,8 @@ async def to_code(config):
var.get_disconnect_trigger(), [], on_disconnect_config var.get_disconnect_trigger(), [], on_disconnect_config
) )
CORE.add_job(final_step)
@automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({})) @automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({}))
async def wifi_connected_to_code(config, condition_id, template_arg, args): async def wifi_connected_to_code(config, condition_id, template_arg, args):
@@ -468,6 +470,28 @@ async def wifi_disable_to_code(config, action_id, template_arg, args):
return cg.new_Pvariable(action_id, template_arg) return cg.new_Pvariable(action_id, template_arg)
KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results"
def request_wifi_scan_results():
"""Request that WiFi scan results be kept in memory after connection.
Components that need access to scan results after WiFi is connected should
call this function during their code generation. This prevents the WiFi component from
freeing scan result memory after successful connection.
"""
CORE.data[KEEP_SCAN_RESULTS_KEY] = True
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step():
"""Final code generation step to configure scan result retention."""
if CORE.data.get(KEEP_SCAN_RESULTS_KEY, False):
cg.add(
cg.RawExpression("wifi::global_wifi_component->set_keep_scan_results(true)")
)
@automation.register_action( @automation.register_action(
"wifi.configure", "wifi.configure",
WiFiConfigureAction, WiFiConfigureAction,

View File

@@ -549,7 +549,7 @@ void WiFiComponent::start_scanning() {
// Using insertion sort instead of std::stable_sort saves flash memory // Using insertion sort instead of std::stable_sort saves flash memory
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) // by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
// IMPORTANT: This sort is stable (preserves relative order of equal elements) // IMPORTANT: This sort is stable (preserves relative order of equal elements)
static void insertion_sort_scan_results(std::vector<WiFiScanResult> &results) { template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
const size_t size = results.size(); const size_t size = results.size();
for (size_t i = 1; i < size; i++) { for (size_t i = 1; i < size; i++) {
// Make a copy to avoid issues with move semantics during comparison // Make a copy to avoid issues with move semantics during comparison
@@ -713,6 +713,12 @@ void WiFiComponent::check_connecting_finished() {
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
this->num_retried_ = 0; this->num_retried_ = 0;
// Free scan results memory unless a component needs them
if (!this->keep_scan_results_) {
this->scan_result_.clear();
this->scan_result_.shrink_to_fit();
}
if (this->fast_connect_) { if (this->fast_connect_) {
this->save_fast_connect_settings_(); this->save_fast_connect_settings_();
} }

View File

@@ -121,6 +121,14 @@ struct EAPAuth {
using bssid_t = std::array<uint8_t, 6>; using bssid_t = std::array<uint8_t, 6>;
// Use std::vector for RP2040 since scan count is unknown (callback-based)
// Use FixedVector for other platforms where count is queried first
#ifdef USE_RP2040
template<typename T> using wifi_scan_vector_t = std::vector<T>;
#else
template<typename T> using wifi_scan_vector_t = FixedVector<T>;
#endif
class WiFiAP { class WiFiAP {
public: public:
void set_ssid(const std::string &ssid); void set_ssid(const std::string &ssid);
@@ -278,7 +286,7 @@ class WiFiComponent : public Component {
const std::string &get_use_address() const; const std::string &get_use_address() const;
void set_use_address(const std::string &use_address); void set_use_address(const std::string &use_address);
const std::vector<WiFiScanResult> &get_scan_result() const { return scan_result_; } const wifi_scan_vector_t<WiFiScanResult> &get_scan_result() const { return scan_result_; }
network::IPAddress wifi_soft_ap_ip(); network::IPAddress wifi_soft_ap_ip();
@@ -316,6 +324,7 @@ class WiFiComponent : public Component {
int8_t wifi_rssi(); int8_t wifi_rssi();
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; }
Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }; Trigger<> *get_connect_trigger() const { return this->connect_trigger_; };
Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; }; Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; };
@@ -385,7 +394,7 @@ class WiFiComponent : public Component {
std::string use_address_; std::string use_address_;
std::vector<WiFiAP> sta_; std::vector<WiFiAP> sta_;
std::vector<WiFiSTAPriority> sta_priorities_; std::vector<WiFiSTAPriority> sta_priorities_;
std::vector<WiFiScanResult> scan_result_; wifi_scan_vector_t<WiFiScanResult> scan_result_;
WiFiAP selected_ap_; WiFiAP selected_ap_;
WiFiAP ap_; WiFiAP ap_;
optional<float> output_power_; optional<float> output_power_;
@@ -424,6 +433,7 @@ class WiFiComponent : public Component {
#endif #endif
bool enable_on_boot_; bool enable_on_boot_;
bool got_ipv4_address_{false}; bool got_ipv4_address_{false};
bool keep_scan_results_{false};
// Pointers at the end (naturally aligned) // Pointers at the end (naturally aligned)
Trigger<> *connect_trigger_{new Trigger<>()}; Trigger<> *connect_trigger_{new Trigger<>()};

View File

@@ -696,7 +696,15 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
this->retry_connect(); this->retry_connect();
return; return;
} }
// Count the number of results first
auto *head = reinterpret_cast<bss_info *>(arg); auto *head = reinterpret_cast<bss_info *>(arg);
size_t count = 0;
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
count++;
}
this->scan_result_.init(count);
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi, std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi,

View File

@@ -784,7 +784,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
} }
records.resize(number); records.resize(number);
scan_result_.reserve(number); scan_result_.init(number);
for (int i = 0; i < number; i++) { for (int i = 0; i < number; i++) {
auto &record = records[i]; auto &record = records[i];
bssid_t bssid; bssid_t bssid;

View File

@@ -411,7 +411,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
if (num < 0) if (num < 0)
return; return;
this->scan_result_.reserve(static_cast<unsigned int>(num)); this->scan_result_.init(static_cast<unsigned int>(num));
for (int i = 0; i < num; i++) { for (int i = 0; i < num; i++) {
String ssid = WiFi.SSID(i); String ssid = WiFi.SSID(i);
wifi_auth_mode_t authmode = WiFi.encryptionType(i); wifi_auth_mode_t authmode = WiFi.encryptionType(i);

View File

@@ -1,5 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import text_sensor from esphome.components import text_sensor, wifi
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BSSID, CONF_BSSID,
@@ -77,7 +77,9 @@ async def to_code(config):
await setup_conf(config, CONF_SSID) await setup_conf(config, CONF_SSID)
await setup_conf(config, CONF_BSSID) await setup_conf(config, CONF_BSSID)
await setup_conf(config, CONF_MAC_ADDRESS) await setup_conf(config, CONF_MAC_ADDRESS)
if CONF_SCAN_RESULTS in config:
await setup_conf(config, CONF_SCAN_RESULTS) await setup_conf(config, CONF_SCAN_RESULTS)
wifi.request_wifi_scan_results()
await setup_conf(config, CONF_DNS_ADDRESS) await setup_conf(config, CONF_DNS_ADDRESS)
if conf := config.get(CONF_IP_ADDRESS): if conf := config.get(CONF_IP_ADDRESS):
wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS]) wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])

View File

@@ -7,6 +7,7 @@
#include "esphome/core/preferences.h" #include "esphome/core/preferences.h"
#include "esphome/core/scheduler.h" #include "esphome/core/scheduler.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include <vector> #include <vector>
@@ -14,7 +15,7 @@ namespace esphome {
template<typename... Ts> class AndCondition : public Condition<Ts...> { template<typename... Ts> class AndCondition : public Condition<Ts...> {
public: public:
explicit AndCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {} explicit AndCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
bool check(Ts... x) override { bool check(Ts... x) override {
for (auto *condition : this->conditions_) { for (auto *condition : this->conditions_) {
if (!condition->check(x...)) if (!condition->check(x...))
@@ -25,12 +26,12 @@ template<typename... Ts> class AndCondition : public Condition<Ts...> {
} }
protected: protected:
std::vector<Condition<Ts...> *> conditions_; FixedVector<Condition<Ts...> *> conditions_;
}; };
template<typename... Ts> class OrCondition : public Condition<Ts...> { template<typename... Ts> class OrCondition : public Condition<Ts...> {
public: public:
explicit OrCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {} explicit OrCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
bool check(Ts... x) override { bool check(Ts... x) override {
for (auto *condition : this->conditions_) { for (auto *condition : this->conditions_) {
if (condition->check(x...)) if (condition->check(x...))
@@ -41,7 +42,7 @@ template<typename... Ts> class OrCondition : public Condition<Ts...> {
} }
protected: protected:
std::vector<Condition<Ts...> *> conditions_; FixedVector<Condition<Ts...> *> conditions_;
}; };
template<typename... Ts> class NotCondition : public Condition<Ts...> { template<typename... Ts> class NotCondition : public Condition<Ts...> {
@@ -55,7 +56,7 @@ template<typename... Ts> class NotCondition : public Condition<Ts...> {
template<typename... Ts> class XorCondition : public Condition<Ts...> { template<typename... Ts> class XorCondition : public Condition<Ts...> {
public: public:
explicit XorCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {} explicit XorCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
bool check(Ts... x) override { bool check(Ts... x) override {
size_t result = 0; size_t result = 0;
for (auto *condition : this->conditions_) { for (auto *condition : this->conditions_) {
@@ -66,7 +67,7 @@ template<typename... Ts> class XorCondition : public Condition<Ts...> {
} }
protected: protected:
std::vector<Condition<Ts...> *> conditions_; FixedVector<Condition<Ts...> *> conditions_;
}; };
template<typename... Ts> class LambdaCondition : public Condition<Ts...> { template<typename... Ts> class LambdaCondition : public Condition<Ts...> {

View File

@@ -197,6 +197,18 @@ template<typename T> class FixedVector {
public: public:
FixedVector() = default; FixedVector() = default;
/// Constructor from initializer list - allocates exact size needed
/// This enables brace initialization: FixedVector<int> v = {1, 2, 3};
FixedVector(std::initializer_list<T> init_list) {
init(init_list.size());
size_t idx = 0;
for (const auto &item : init_list) {
new (data_ + idx) T(item);
++idx;
}
size_ = init_list.size();
}
~FixedVector() { cleanup_(); } ~FixedVector() { cleanup_(); }
// Disable copy operations (avoid accidental expensive copies) // Disable copy operations (avoid accidental expensive copies)

View File

@@ -1,3 +1,4 @@
[build] [build]
command = "script/build-api-docs" command = "script/build-api-docs"
publish = "api-docs" publish = "api-docs"
environment = { PYTHON_VERSION = "3.13" }

View File

@@ -12,13 +12,13 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0 esptool==5.1.0
click==8.1.7 click==8.1.7
esphome-dashboard==20251013.0 esphome-dashboard==20251013.0
aioesphomeapi==41.16.1 aioesphomeapi==41.17.0
zeroconf==0.148.0 zeroconf==0.148.0
puremagic==1.30 puremagic==1.30
ruamel.yaml==0.18.15 # dashboard_import ruamel.yaml==0.18.15 # dashboard_import
ruamel.yaml.clib==0.2.12 # dashboard_import ruamel.yaml.clib==0.2.14 # dashboard_import
esphome-glyphsets==0.2.0 esphome-glyphsets==0.2.0
pillow==10.4.0 pillow==11.3.0
cairosvg==2.8.2 cairosvg==2.8.2
freetype-py==2.5.1 freetype-py==2.5.1
jinja2==3.1.6 jinja2==3.1.6

View File

@@ -1,6 +1,7 @@
substitutions: substitutions:
tx_pin: GPIO4 tx_pin: GPIO4
rx_pin: GPIO5 rx_pin: GPIO5
flow_control_pin: GPIO13
packages: packages:
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml

View File

@@ -1,6 +1,7 @@
substitutions: substitutions:
tx_pin: GPIO4 tx_pin: GPIO4
rx_pin: GPIO5 rx_pin: GPIO5
flow_control_pin: GPIO13
packages: packages:
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml

View File

@@ -1,6 +1,7 @@
substitutions: substitutions:
tx_pin: GPIO4 tx_pin: GPIO4
rx_pin: GPIO5 rx_pin: GPIO5
flow_control_pin: GPIO13
packages: packages:
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml