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:
@@ -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
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -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"];
|
||||
|
@@ -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()));
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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{};
|
||||
|
@@ -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
|
||||
|
@@ -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):
|
||||
|
@@ -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()
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#include "binary_sensor.h"
|
||||
#include "nfc_binary_sensor.h"
|
||||
#include "../nfc_helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
@@ -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:
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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_() {
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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];
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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()); }
|
||||
};
|
||||
|
||||
///@}
|
||||
|
@@ -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)
|
||||
|
@@ -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: .*",
|
||||
|
@@ -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",
|
||||
)
|
||||
)
|
||||
|
@@ -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
|
||||
|
@@ -1 +1,6 @@
|
||||
<<: !include common_v2.yaml
|
||||
|
||||
web_server:
|
||||
auth:
|
||||
username: admin
|
||||
password: password
|
||||
|
Reference in New Issue
Block a user