1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-26 23:22:21 +01:00

Merge branch 'dev' into fold_ring_buffer_esp32_ble_tracker

This commit is contained in:
Jesse Hills
2025-08-04 17:00:08 +12:00
committed by GitHub
29 changed files with 357 additions and 162 deletions

View File

@@ -168,6 +168,8 @@ This document provides essential context for AI models interacting with this pro
* `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers.
* `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting.
* **CI/CD Pipeline:** Defined in `.github/workflows`.
* **Static Analysis & Development:**
* `esphome/core/defines.h`: A comprehensive header file containing all `#define` directives that can be added by components using `cg.add_define()` in Python. This file is used exclusively for development, static analysis tools, and CI testing - it is not used during runtime compilation. When developing components that add new defines, they must be added to this file to ensure proper IDE support and static analysis coverage. The file includes feature flags, build configurations, and platform-specific defines that help static analyzers understand the complete codebase without needing to compile for specific platforms.
## 6. Development & Testing Workflow

View File

@@ -277,20 +277,20 @@ def upload_using_esptool(config, port, file, speed):
def run_esptool(baud_rate):
cmd = [
"esptool.py",
"esptool",
"--before",
"default_reset",
"default-reset",
"--after",
"hard_reset",
"hard-reset",
"--baud",
str(baud_rate),
"--port",
port,
"--chip",
mcu,
"write_flash",
"write-flash",
"-z",
"--flash_size",
"--flash-size",
"detect",
]
for img in flash_images:

View File

@@ -250,8 +250,8 @@ message DeviceInfoResponse {
// Supports receiving and saving api encryption key
bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"];
repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES"];
repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS"];
repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES", (fixed_array_size_define) = "ESPHOME_DEVICE_COUNT"];
repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS", (fixed_array_size_define) = "ESPHOME_AREA_COUNT"];
// Top-level area info to phase out suggested_area
AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"];

View File

@@ -1462,18 +1462,22 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
resp.api_encryption_supported = true;
#endif
#ifdef USE_DEVICES
size_t device_index = 0;
for (auto const &device : App.get_devices()) {
resp.devices.emplace_back();
auto &device_info = resp.devices.back();
if (device_index >= ESPHOME_DEVICE_COUNT)
break;
auto &device_info = resp.devices[device_index++];
device_info.device_id = device->get_device_id();
device_info.set_name(StringRef(device->get_name()));
device_info.area_id = device->get_area_id();
}
#endif
#ifdef USE_AREAS
size_t area_index = 0;
for (auto const &area : App.get_areas()) {
resp.areas.emplace_back();
auto &area_info = resp.areas.back();
if (area_index >= ESPHOME_AREA_COUNT)
break;
auto &area_info = resp.areas[area_index++];
area_info.area_id = area->get_area_id();
area_info.set_name(StringRef(area->get_name()));
}

View File

@@ -115,12 +115,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(19, this->api_encryption_supported);
#endif
#ifdef USE_DEVICES
for (auto &it : this->devices) {
for (const auto &it : this->devices) {
buffer.encode_message(20, it, true);
}
#endif
#ifdef USE_AREAS
for (auto &it : this->areas) {
for (const auto &it : this->areas) {
buffer.encode_message(21, it, true);
}
#endif
@@ -167,10 +167,14 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const {
size.add_bool(2, this->api_encryption_supported);
#endif
#ifdef USE_DEVICES
size.add_repeated_message(2, this->devices);
for (const auto &it : this->devices) {
size.add_message_object_force(2, it);
}
#endif
#ifdef USE_AREAS
size.add_repeated_message(2, this->areas);
for (const auto &it : this->areas) {
size.add_message_object_force(2, it);
}
#endif
#ifdef USE_AREAS
size.add_message_object(2, this->area);

View File

@@ -490,7 +490,7 @@ class DeviceInfo : public ProtoMessage {
class DeviceInfoResponse : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 10;
static constexpr uint8_t ESTIMATED_SIZE = 211;
static constexpr uint8_t ESTIMATED_SIZE = 247;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "device_info_response"; }
#endif
@@ -543,10 +543,10 @@ class DeviceInfoResponse : public ProtoMessage {
bool api_encryption_supported{false};
#endif
#ifdef USE_DEVICES
std::vector<DeviceInfo> devices{};
std::array<DeviceInfo, ESPHOME_DEVICE_COUNT> devices{};
#endif
#ifdef USE_AREAS
std::vector<AreaInfo> areas{};
std::array<AreaInfo, ESPHOME_AREA_COUNT> areas{};
#endif
#ifdef USE_AREAS
AreaInfo area{};

View File

@@ -680,6 +680,64 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
)
class _FrameworkMigrationWarning:
shown = False
def _show_framework_migration_message(name: str, variant: str) -> None:
"""Show a friendly message about framework migration when defaulting to Arduino."""
if _FrameworkMigrationWarning.shown:
return
_FrameworkMigrationWarning.shown = True
from esphome.log import AnsiFore, color
message = (
color(
AnsiFore.BOLD_CYAN,
f"💡 IMPORTANT: {name} doesn't have a framework specified!",
)
+ "\n\n"
+ f"Currently, {variant} defaults to the Arduino framework.\n"
+ color(AnsiFore.YELLOW, "This will change to ESP-IDF in ESPHome 2026.1.0.\n")
+ "\n"
+ "Note: Newer ESP32 variants (C6, H2, P4, etc.) already use ESP-IDF by default.\n"
+ "\n"
+ "Why change? ESP-IDF offers:\n"
+ color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n")
+ color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n")
+ color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n")
+ color(
AnsiFore.GREEN,
" 🔧 Active development and testing by ESPHome developers\n",
)
+ "\n"
+ "Trade-offs:\n"
+ color(AnsiFore.YELLOW, " ⏱️ Compile times are ~25% longer\n")
+ color(AnsiFore.YELLOW, " 🔄 Some components need migration\n")
+ "\n"
+ "What should I do?\n"
+ color(AnsiFore.CYAN, " Option 1")
+ ": Migrate to ESP-IDF (recommended)\n"
+ " Add this to your YAML under 'esp32:':\n"
+ color(AnsiFore.WHITE, " framework:\n")
+ color(AnsiFore.WHITE, " type: esp-idf\n")
+ "\n"
+ color(AnsiFore.CYAN, " Option 2")
+ ": Keep using Arduino (still supported)\n"
+ " Add this to your YAML under 'esp32:':\n"
+ color(AnsiFore.WHITE, " framework:\n")
+ color(AnsiFore.WHITE, " type: arduino\n")
+ "\n"
+ "Need help? Check out the migration guide:\n"
+ color(
AnsiFore.BLUE,
"https://esphome.io/guides/esp32_arduino_to_idf.html",
)
)
_LOGGER.warning(message)
def _set_default_framework(config):
if CONF_FRAMEWORK not in config:
config = config.copy()
@@ -688,6 +746,10 @@ def _set_default_framework(config):
if variant in ARDUINO_ALLOWED_VARIANTS:
config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({})
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
# Show the migration message
_show_framework_migration_message(
config.get(CONF_NAME, "This device"), variant
)
else:
config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({})
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF

View File

@@ -93,8 +93,8 @@ def merge_factory_bin(source, target, env):
"esptool",
"--chip",
chip,
"merge_bin",
"--flash_size",
"merge-bin",
"--flash-size",
flash_size,
"--output",
str(output_path),
@@ -110,7 +110,7 @@ def merge_factory_bin(source, target, env):
if result == 0:
print(f"Successfully created {output_path}")
else:
print(f"Error: esptool merge_bin failed with code {result}")
print(f"Error: esptool merge-bin failed with code {result}")
def esp32_copy_ota_bin(source, target, env):

View File

@@ -15,6 +15,7 @@ from freetype import (
FT_LOAD_RENDER,
FT_LOAD_TARGET_MONO,
Face,
FT_Exception,
ft_pixel_mode_mono,
)
import requests
@@ -94,7 +95,14 @@ class FontCache(MutableMapping):
return self.store[self._keytransform(item)]
def __setitem__(self, key, value):
self.store[self._keytransform(key)] = Face(str(value))
transformed = self._keytransform(key)
try:
self.store[transformed] = Face(str(value))
except FT_Exception as exc:
file = transformed.split(":", 1)
raise cv.Invalid(
f"{file[0].capitalize()} {file[1]} is not a valid font file"
) from exc
FONT_CACHE = FontCache()

View File

@@ -1,4 +1,4 @@
#include "binary_sensor.h"
#include "nfc_binary_sensor.h"
#include "../nfc_helpers.h"
#include "esphome/core/log.h"

View File

@@ -298,6 +298,7 @@ async def to_code(config):
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
if CONF_AUTH in config:
cg.add_define("USE_WEBSERVER_AUTH")
cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME]))
cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD]))
if CONF_CSS_INCLUDE in config:

View File

@@ -376,23 +376,32 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
}
#endif
#define set_json_id(root, obj, sensor, start_config) \
(root)["id"] = sensor; \
if (((start_config) == DETAIL_ALL)) { \
(root)["name"] = (obj)->get_name(); \
(root)["icon"] = (obj)->get_icon(); \
(root)["entity_category"] = (obj)->get_entity_category(); \
if ((obj)->is_disabled_by_default()) \
(root)["is_disabled_by_default"] = (obj)->is_disabled_by_default(); \
// Helper functions to reduce code size by avoiding macro expansion
static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id, JsonDetail start_config) {
root["id"] = id;
if (start_config == DETAIL_ALL) {
root["name"] = obj->get_name();
root["icon"] = obj->get_icon();
root["entity_category"] = obj->get_entity_category();
bool is_disabled = obj->is_disabled_by_default();
if (is_disabled)
root["is_disabled_by_default"] = is_disabled;
}
}
#define set_json_value(root, obj, sensor, value, start_config) \
set_json_id((root), (obj), sensor, start_config); \
(root)["value"] = value;
template<typename T>
static void set_json_value(JsonObject &root, EntityBase *obj, const std::string &id, const T &value,
JsonDetail start_config) {
set_json_id(root, obj, id, start_config);
root["value"] = value;
}
#define set_json_icon_state_value(root, obj, sensor, state, value, start_config) \
set_json_value(root, obj, sensor, value, start_config); \
(root)["state"] = state;
template<typename T>
static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const std::string &id,
const std::string &state, const T &value, JsonDetail start_config) {
set_json_value(root, obj, id, value, start_config);
root["state"] = state;
}
// Helper to get request detail parameter
static JsonDetail get_request_detail(AsyncWebServerRequest *request) {

View File

@@ -14,9 +14,11 @@ WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-av
void WebServerBase::add_handler(AsyncWebHandler *handler) {
// remove all handlers
#ifdef USE_WEBSERVER_AUTH
if (!credentials_.username.empty()) {
handler = new internal::AuthMiddlewareHandler(handler, &credentials_);
}
#endif
this->handlers_.push_back(handler);
if (this->server_ != nullptr) {
this->server_->addHandler(handler);

View File

@@ -41,6 +41,7 @@ class MiddlewareHandler : public AsyncWebHandler {
AsyncWebHandler *next_;
};
#ifdef USE_WEBSERVER_AUTH
struct Credentials {
std::string username;
std::string password;
@@ -79,6 +80,7 @@ class AuthMiddlewareHandler : public MiddlewareHandler {
protected:
Credentials *credentials_;
};
#endif
} // namespace internal
@@ -108,8 +110,10 @@ class WebServerBase : public Component {
std::shared_ptr<AsyncWebServer> get_server() const { return server_; }
float get_setup_priority() const override;
#ifdef USE_WEBSERVER_AUTH
void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); }
void set_auth_password(std::string auth_password) { credentials_.password = std::move(auth_password); }
#endif
void add_handler(AsyncWebHandler *handler);
@@ -121,7 +125,9 @@ class WebServerBase : public Component {
uint16_t port_{80};
std::shared_ptr<AsyncWebServer> server_{nullptr};
std::vector<AsyncWebHandler *> handlers_;
#ifdef USE_WEBSERVER_AUTH
internal::Credentials credentials_;
#endif
};
} // namespace web_server_base

View File

@@ -223,6 +223,7 @@ void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code
this->rsp_ = rsp;
}
#ifdef USE_WEBSERVER_AUTH
bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const {
if (username == nullptr || password == nullptr || *username == 0) {
return true;
@@ -261,6 +262,7 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
httpd_resp_set_hdr(*this, "WWW-Authenticate", auth_val.c_str());
httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr);
}
#endif
AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) {
auto find = this->params_.find(name);
@@ -423,14 +425,14 @@ void AsyncEventSourceResponse::destroy(void *ptr) {
void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
DeferredEvent item(source, message_generator);
auto iter = std::find_if(this->deferred_queue_.begin(), this->deferred_queue_.end(),
[&item](const DeferredEvent &test) -> bool { return test == item; });
if (iter != this->deferred_queue_.end()) {
(*iter) = item;
} else {
this->deferred_queue_.push_back(item);
// Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
for (auto &event : this->deferred_queue_) {
if (event == item) {
event = item;
return;
}
}
this->deferred_queue_.push_back(item);
}
void AsyncEventSourceResponse::process_deferred_queue_() {

View File

@@ -115,9 +115,11 @@ class AsyncWebServerRequest {
// NOLINTNEXTLINE(readability-identifier-naming)
size_t contentLength() const { return this->req_->content_len; }
#ifdef USE_WEBSERVER_AUTH
bool authenticate(const char *username, const char *password) const;
// NOLINTNEXTLINE(readability-identifier-naming)
void requestAuthentication(const char *realm = nullptr) const;
#endif
void redirect(const std::string &url);

View File

@@ -505,6 +505,54 @@ void WiFiComponent::start_scanning() {
this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING;
}
// Helper function for WiFi scan result comparison
// Returns true if 'a' should be placed before 'b' in the sorted order
[[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) {
// Matching networks always come before non-matching
if (a.get_matches() && !b.get_matches())
return true;
if (!a.get_matches() && b.get_matches())
return false;
if (a.get_matches() && b.get_matches()) {
// For APs with the same SSID, always prefer stronger signal
// This helps with mesh networks and multiple APs
if (a.get_ssid() == b.get_ssid()) {
return a.get_rssi() > b.get_rssi();
}
// For different SSIDs, check priority first
if (a.get_priority() != b.get_priority())
return a.get_priority() > b.get_priority();
// If priorities are equal, prefer stronger signal
return a.get_rssi() > b.get_rssi();
}
// Both don't match - sort by signal strength
return a.get_rssi() > b.get_rssi();
}
// Helper function for insertion sort of WiFi scan results
// Using insertion sort instead of std::stable_sort saves flash memory
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
static void insertion_sort_scan_results(std::vector<WiFiScanResult> &results) {
const size_t size = results.size();
for (size_t i = 1; i < size; i++) {
// Make a copy to avoid issues with move semantics during comparison
WiFiScanResult key = results[i];
int32_t j = i - 1;
// Move elements that are worse than key to the right
// For stability, we only move if key is strictly better than results[j]
while (j >= 0 && wifi_scan_result_is_better(key, results[j])) {
results[j + 1] = results[j];
j--;
}
results[j + 1] = key;
}
}
void WiFiComponent::check_scanning_finished() {
if (!this->scan_done_) {
if (millis() - this->action_started_ > 30000) {
@@ -535,30 +583,8 @@ void WiFiComponent::check_scanning_finished() {
}
}
std::stable_sort(this->scan_result_.begin(), this->scan_result_.end(),
[](const WiFiScanResult &a, const WiFiScanResult &b) {
// return true if a is better than b
if (a.get_matches() && !b.get_matches())
return true;
if (!a.get_matches() && b.get_matches())
return false;
if (a.get_matches() && b.get_matches()) {
// For APs with the same SSID, always prefer stronger signal
// This helps with mesh networks and multiple APs
if (a.get_ssid() == b.get_ssid()) {
return a.get_rssi() > b.get_rssi();
}
// For different SSIDs, check priority first
if (a.get_priority() != b.get_priority())
return a.get_priority() > b.get_priority();
// If priorities are equal, prefer stronger signal
return a.get_rssi() > b.get_rssi();
}
return a.get_rssi() > b.get_rssi();
});
// Sort scan results using insertion sort for better memory efficiency
insertion_sort_scan_results(this->scan_result_);
for (auto &res : this->scan_result_) {
char bssid_s[18];

View File

@@ -87,7 +87,7 @@ from esphome.core import (
TimePeriodNanoseconds,
TimePeriodSeconds,
)
from esphome.helpers import add_class_to_obj, list_starts_with
from esphome.helpers import add_class_to_obj, docs_url, list_starts_with
from esphome.schema_extractors import (
SCHEMA_EXTRACT,
schema_extractor,
@@ -666,14 +666,6 @@ def only_with_framework(
if suggestions is None:
suggestions = {}
version = Version.parse(ESPHOME_VERSION)
if version.is_beta:
docs_format = "https://beta.esphome.io/components/{path}"
elif version.is_dev:
docs_format = "https://next.esphome.io/components/{path}"
else:
docs_format = "https://esphome.io/components/{path}"
def validator_(obj):
if CORE.target_framework not in frameworks:
err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}"
@@ -681,7 +673,7 @@ def only_with_framework(
(component, docs_path) = suggestion
err_str += f"\nPlease use '{component}'"
if docs_path:
err_str += f": {docs_format.format(path=docs_path)}"
err_str += f": {docs_url(path=f'components/{docs_path}')}"
raise Invalid(err_str)
return obj

View File

@@ -34,6 +34,44 @@ namespace esphome {
static const char *const TAG = "app";
// Helper function for insertion sort of components by setup priority
// Using insertion sort instead of std::stable_sort saves ~1.3KB of flash
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
// IMPORTANT: This sort is stable (preserves relative order of equal elements),
// which is necessary to maintain user-defined component order for same priority
template<typename Iterator> static void insertion_sort_by_setup_priority(Iterator first, Iterator last) {
for (auto it = first + 1; it != last; ++it) {
auto key = *it;
float key_priority = key->get_actual_setup_priority();
auto j = it - 1;
// Using '<' (not '<=') ensures stability - equal priority components keep their order
while (j >= first && (*j)->get_actual_setup_priority() < key_priority) {
*(j + 1) = *j;
j--;
}
*(j + 1) = key;
}
}
// Helper function for insertion sort of components by loop priority
// IMPORTANT: This sort is stable (preserves relative order of equal elements),
// which is required when components are re-sorted during setup() if they block
template<typename Iterator> static void insertion_sort_by_loop_priority(Iterator first, Iterator last) {
for (auto it = first + 1; it != last; ++it) {
auto key = *it;
float key_priority = key->get_loop_priority();
auto j = it - 1;
// Using '<' (not '<=') ensures stability - equal priority components keep their order
while (j >= first && (*j)->get_loop_priority() < key_priority) {
*(j + 1) = *j;
j--;
}
*(j + 1) = key;
}
}
void Application::register_component_(Component *comp) {
if (comp == nullptr) {
ESP_LOGW(TAG, "Tried to register null component!");
@@ -51,9 +89,9 @@ void Application::register_component_(Component *comp) {
void Application::setup() {
ESP_LOGI(TAG, "Running through setup()");
ESP_LOGV(TAG, "Sorting components by setup priority");
std::stable_sort(this->components_.begin(), this->components_.end(), [](const Component *a, const Component *b) {
return a->get_actual_setup_priority() > b->get_actual_setup_priority();
});
// Sort by setup priority using our helper function
insertion_sort_by_setup_priority(this->components_.begin(), this->components_.end());
// Initialize looping_components_ early so enable_pending_loops_() works during setup
this->calculate_looping_components_();
@@ -69,8 +107,8 @@ void Application::setup() {
if (component->can_proceed())
continue;
std::stable_sort(this->components_.begin(), this->components_.begin() + i + 1,
[](Component *a, Component *b) { return a->get_loop_priority() > b->get_loop_priority(); });
// Sort components 0 through i by loop priority
insertion_sort_by_loop_priority(this->components_.begin(), this->components_.begin() + i + 1);
do {
uint8_t new_app_state = STATUS_LED_WARNING;
@@ -459,24 +497,25 @@ void Application::unregister_socket_fd(int fd) {
if (fd < 0)
return;
auto it = std::find(this->socket_fds_.begin(), this->socket_fds_.end(), fd);
if (it != this->socket_fds_.end()) {
for (size_t i = 0; i < this->socket_fds_.size(); i++) {
if (this->socket_fds_[i] != fd)
continue;
// Swap with last element and pop - O(1) removal since order doesn't matter
if (it != this->socket_fds_.end() - 1) {
std::swap(*it, this->socket_fds_.back());
}
if (i < this->socket_fds_.size() - 1)
this->socket_fds_[i] = this->socket_fds_.back();
this->socket_fds_.pop_back();
this->socket_fds_changed_ = true;
// Only recalculate max_fd if we removed the current max
if (fd == this->max_fd_) {
if (this->socket_fds_.empty()) {
this->max_fd_ = -1;
} else {
// Find new max using std::max_element
this->max_fd_ = *std::max_element(this->socket_fds_.begin(), this->socket_fds_.end());
this->max_fd_ = -1;
for (int sock_fd : this->socket_fds_) {
if (sock_fd > this->max_fd_)
this->max_fd_ = sock_fd;
}
}
return;
}
}

View File

@@ -101,12 +101,9 @@ class Application {
arch_init();
this->name_add_mac_suffix_ = name_add_mac_suffix;
if (name_add_mac_suffix) {
this->name_ = name + "-" + get_mac_address().substr(6);
if (friendly_name.empty()) {
this->friendly_name_ = "";
} else {
this->friendly_name_ = friendly_name + " " + get_mac_address().substr(6);
}
const std::string mac_suffix = get_mac_address().substr(6);
this->name_ = name + "-" + mac_suffix;
this->friendly_name_ = friendly_name.empty() ? "" : friendly_name + " " + mac_suffix;
} else {
this->name_ = name;
this->friendly_name_ = friendly_name;
@@ -214,14 +211,6 @@ class Application {
#endif
/// Reserve space for components to avoid memory fragmentation
void reserve_components(size_t count) { this->components_.reserve(count); }
#ifdef USE_AREAS
void reserve_area(size_t count) { this->areas_.reserve(count); }
#endif
#ifdef USE_DEVICES
void reserve_device(size_t count) { this->devices_.reserve(count); }
#endif
/// Register the component in this Application instance.
template<class C> C *register_component(C *c) {
@@ -316,7 +305,7 @@ class Application {
} \
return nullptr; \
}
const std::vector<Device *> &get_devices() { return this->devices_; }
const auto &get_devices() { return this->devices_; }
#else
#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \
entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \
@@ -328,7 +317,7 @@ class Application {
}
#endif // USE_DEVICES
#ifdef USE_AREAS
const std::vector<Area *> &get_areas() { return this->areas_; }
const auto &get_areas() { return this->areas_; }
#endif
#ifdef USE_BINARY_SENSOR
auto &get_binary_sensors() const { return this->binary_sensors_; }
@@ -462,12 +451,7 @@ class Application {
const char *comment_{nullptr};
const char *compilation_time_{nullptr};
// size_t members
size_t dump_config_at_{SIZE_MAX};
// Vectors (largest members)
std::vector<Component *> components_{};
// std::vector (3 pointers each: begin, end, capacity)
// Partitioned vector design for looping components
// =================================================
// Components are partitioned into [active | inactive] sections:
@@ -485,12 +469,54 @@ class Application {
// and active_end_ is incremented
// - This eliminates branch mispredictions from flag checking in the hot loop
std::vector<Component *> looping_components_{};
#ifdef USE_SOCKET_SELECT_SUPPORT
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
#endif
// std::string members (typically 24-32 bytes each)
std::string name_;
std::string friendly_name_;
// size_t members
size_t dump_config_at_{SIZE_MAX};
// 4-byte members
uint32_t last_loop_{0};
uint32_t loop_component_start_time_{0};
#ifdef USE_SOCKET_SELECT_SUPPORT
int max_fd_{-1}; // Highest file descriptor number for select()
#endif
// 2-byte members (grouped together for alignment)
uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds)
uint16_t looping_components_active_end_{0}; // Index marking end of active components in looping_components_
uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration
// 1-byte members (grouped together to minimize padding)
uint8_t app_state_{0};
bool name_add_mac_suffix_;
bool in_loop_{false};
volatile bool has_pending_enable_loop_requests_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT
// Variable-sized members
fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes
fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_
#endif
// StaticVectors (largest members - contain actual array data inline)
StaticVector<Component *, ESPHOME_COMPONENT_COUNT> components_{};
#ifdef USE_DEVICES
std::vector<Device *> devices_{};
StaticVector<Device *, ESPHOME_DEVICE_COUNT> devices_{};
#endif
#ifdef USE_AREAS
std::vector<Area *> areas_{};
StaticVector<Area *, ESPHOME_AREA_COUNT> areas_{};
#endif
#ifdef USE_BINARY_SENSOR
StaticVector<binary_sensor::BinarySensor *, ESPHOME_ENTITY_BINARY_SENSOR_COUNT> binary_sensors_{};
@@ -556,41 +582,6 @@ class Application {
#ifdef USE_UPDATE
StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{};
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
#endif
// String members
std::string name_;
std::string friendly_name_;
// 4-byte members
uint32_t last_loop_{0};
uint32_t loop_component_start_time_{0};
#ifdef USE_SOCKET_SELECT_SUPPORT
int max_fd_{-1}; // Highest file descriptor number for select()
#endif
// 2-byte members (grouped together for alignment)
uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds)
uint16_t looping_components_active_end_{0};
uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration
// 1-byte members (grouped together to minimize padding)
uint8_t app_state_{0};
bool name_add_mac_suffix_;
bool in_loop_{false};
volatile bool has_pending_enable_loop_requests_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes
// Variable-sized members at end
fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes
fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_
#endif
};
/// Global storage of Application pointer - only one Application can exist.

View File

@@ -459,10 +459,8 @@ async def to_code(config: ConfigType) -> None:
config[CONF_NAME_ADD_MAC_SUFFIX],
)
)
# Reserve space for components to avoid reallocation during registration
cg.add(
cg.RawStatement(f"App.reserve_components({len(CORE.component_ids)});"),
)
# Define component count for static allocation
cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids))
CORE.add_job(_add_platform_defines)
@@ -531,8 +529,8 @@ async def to_code(config: ConfigType) -> None:
all_areas.extend(config[CONF_AREAS])
if all_areas:
cg.add(cg.RawStatement(f"App.reserve_area({len(all_areas)});"))
cg.add_define("USE_AREAS")
cg.add_define("ESPHOME_AREA_COUNT", len(all_areas))
for area_conf in all_areas:
area_id: core.ID = area_conf[CONF_ID]
@@ -549,9 +547,9 @@ async def to_code(config: ConfigType) -> None:
if not devices:
return
# Reserve space for devices
cg.add(cg.RawStatement(f"App.reserve_device({len(devices)});"))
# Define device count for static allocation
cg.add_define("USE_DEVICES")
cg.add_define("ESPHOME_DEVICE_COUNT", len(devices))
# Process each device
for dev_conf in devices:

View File

@@ -163,6 +163,7 @@
#define USE_SPI
#define USE_VOICE_ASSISTANT
#define USE_WEBSERVER
#define USE_WEBSERVER_AUTH
#define USE_WEBSERVER_OTA
#define USE_WEBSERVER_PORT 80 // NOLINT
#define USE_WEBSERVER_SORTING
@@ -210,6 +211,7 @@
{}
#define USE_WEBSERVER
#define USE_WEBSERVER_AUTH
#define USE_WEBSERVER_PORT 80 // NOLINT
#endif
@@ -226,6 +228,7 @@
#define USE_SOCKET_IMPL_LWIP_SOCKETS
#define USE_SOCKET_SELECT_SUPPORT
#define USE_WEBSERVER
#define USE_WEBSERVER_AUTH
#define USE_WEBSERVER_PORT 80 // NOLINT
#endif
@@ -240,7 +243,10 @@
#define USE_DASHBOARD_IMPORT
// Default entity counts for static analysis
// Default counts for static analysis
#define ESPHOME_COMPONENT_COUNT 50
#define ESPHOME_DEVICE_COUNT 10
#define ESPHOME_AREA_COUNT 10
#define ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT 1
#define ESPHOME_ENTITY_BINARY_SENSOR_COUNT 1
#define ESPHOME_ENTITY_BUTTON_COUNT 1

View File

@@ -5,6 +5,7 @@
#include <cstdint>
#include <cstring>
#include <functional>
#include <iterator>
#include <limits>
#include <memory>
#include <string>
@@ -100,6 +101,8 @@ template<typename T, size_t N> class StaticVector {
using value_type = T;
using iterator = typename std::array<T, N>::iterator;
using const_iterator = typename std::array<T, N>::const_iterator;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;
private:
std::array<T, N> data_{};
@@ -114,6 +117,7 @@ template<typename T, size_t N> class StaticVector {
}
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; }
@@ -123,6 +127,12 @@ template<typename T, size_t N> class StaticVector {
iterator end() { return data_.begin() + count_; }
const_iterator begin() const { return data_.begin(); }
const_iterator end() const { return data_.begin() + count_; }
// Reverse iterators
reverse_iterator rbegin() { return reverse_iterator(end()); }
reverse_iterator rend() { return reverse_iterator(begin()); }
const_reverse_iterator rbegin() const { return const_reverse_iterator(end()); }
const_reverse_iterator rend() const { return const_reverse_iterator(begin()); }
};
///@}

View File

@@ -9,6 +9,8 @@ import re
import tempfile
from urllib.parse import urlparse
from esphome.const import __version__ as ESPHOME_VERSION
_LOGGER = logging.getLogger(__name__)
IS_MACOS = platform.system() == "Darwin"
@@ -503,3 +505,20 @@ _DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9-_]")
def sanitize(value):
"""Same behaviour as `helpers.cpp` method `str_sanitize`."""
return _DISALLOWED_CHARS.sub("_", value)
def docs_url(path: str) -> str:
"""Return the URL to the documentation for a given path."""
# Local import to avoid circular import
from esphome.config_validation import Version
version = Version.parse(ESPHOME_VERSION)
if version.is_beta:
docs_format = "https://beta.esphome.io/{path}"
elif version.is_dev:
docs_format = "https://next.esphome.io/{path}"
else:
docs_format = "https://esphome.io/{path}"
path = path.removeprefix("/")
return docs_format.format(path=path)

View File

@@ -61,6 +61,7 @@ FILTER_PLATFORMIO_LINES = [
r"Advanced Memory Usage is available via .*",
r"Merged .* ELF section",
r"esptool.py v.*",
r"esptool v.*",
r"Checking size .*",
r"Retrieving maximum program size .*",
r"PLATFORM: .*",

View File

@@ -345,5 +345,11 @@ def get_esp32_arduino_flash_error_help() -> str | None:
+ "2. Clean build files and compile again\n"
+ "\n"
+ "Note: ESP-IDF uses less flash space and provides better performance.\n"
+ "Some Arduino-specific libraries may need alternatives.\n\n"
+ "Some Arduino-specific libraries may need alternatives.\n"
+ "\n"
+ "For detailed migration instructions, see:\n"
+ color(
AnsiFore.BLUE,
"https://esphome.io/guides/esp32_arduino_to_idf.html\n\n",
)
)

View File

@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.0.2
click==8.1.7
esphome-dashboard==20250514.0
aioesphomeapi==37.2.3
aioesphomeapi==37.2.4
zeroconf==0.147.0
puremagic==1.30
ruamel.yaml==0.18.14 # dashboard_import

View File

@@ -1 +1,6 @@
<<: !include common_v2.yaml
web_server:
auth:
username: admin
password: password