Compare commits

..

18 Commits

Author SHA1 Message Date
J. Nick Koston
4ea417966d [core] Poison brace-depth tracking once it goes negative
Address Copilot review: the prior comment promised that a negative depth would never re-enable splitting, but an arithmetically-balanced later { could bring depth back to 0 and resume flushing mid-stream. Track an explicit 'poisoned' flag set once depth < 0 that permanently disables further flushes for the rest of the input. Adds a regression test where a leading } and a later { would have re-enabled splitting without the poison flag.
2026-04-17 19:01:12 -05:00
J. Nick Koston
6446f309c1 [core] Replace mutable-list-flag pattern with _ComponentGroup dataclass
The mutable-list-of-bool trick for rebinding-safe flag mutation works but reads poorly. Replace with a _ComponentGroup dataclass carrying lines + unsafe + no_split fields. cpp_main_section now reads as a straight iteration over typed groups; no apologetic comments needed.
2026-04-17 18:56:59 -05:00
J. Nick Koston
093c34d4a4 [core] Review cleanup: docstring accuracy, rationale comments, unsafe+no_split test
- Fix the ComponentMarker docstring's incomplete 'either placement-news or mutates a global' claim — acknowledge that function-local patterns also exist and note the bare-local detection covers them.
- Document that _emits_bare_local's RawExpression detection is intentionally safety-biased: false negatives break compilation, false positives just keep a slightly larger IIFE. Note the CallExpression(..., RawExpression) negative case explicitly.
- Explain the mutable-list-flag pattern in cpp_main_section — dataclass would read cleaner but the pattern is localized.
- Add regression test for a group with BOTH IIFEUnsafeStatement and a bare-local: unsafe wins (flat emission) because a return inside any IIFE, even a single big one, only exits the lambda.
2026-04-17 18:55:39 -05:00
J. Nick Koston
3ab935bebb [core] Expose IIFE_MAX_STATEMENTS constant and derive test sizes from it
Tests were hardcoding 120 statements and expecting 3 sub-chunks from a 50-cap. Extract the cap as a named module constant and compute the test-input size from it, so bumping the cap doesn't silently invalidate the tests.
2026-04-17 18:50:41 -05:00
J. Nick Koston
bb0067f517 [core] Strengthen RawExpression-as-arg test
Exercise the actual CallExpression(..., RawExpression) pattern that components use for passing raw arguments. The previous test used RawStatement filler which didn't exercise the detection path we wanted to assert doesn't trigger.
2026-04-17 18:49:04 -05:00
J. Nick Koston
550f6e7c72 [core] Detect bare-local emission and disable sub-split for those groups
A component's group is wrapped in a single IIFE with no sub-splitting when its to_code emits any of: scope-brace RawStatement, direct RawExpression via cg.add (raw bare-local or field-assignment like 'tz.field = x'), or typed AssignmentExpression (cg.variable). Detection is content-aware so entity_helpers' inline-comment RawStatements and RawExpression-as-CallExpression-arg don't false-positive. Adds 4 regression tests covering each detection path and the non-triggering inverse cases.
2026-04-17 18:46:30 -05:00
J. Nick Koston
5f2582efcd [safe_mode] Fix setup()-exit return getting trapped in IIFE
safe_mode emits `if (should_enter_safe_mode(...)) return` via
cg.add(RawExpression(...)) to short-circuit the rest of setup() and
boot into safe mode. With setup() split into per-component IIFEs,
that `return` was only exiting the lambda, so the rest of setup() ran
anyway — breaking safe-mode recovery.

Add IIFEUnsafeStatement, a Statement wrapper that marks its containing
component's block for flat emission (no IIFE). safe_mode wraps its
return expression in it. cpp_main_section detects any such statement
in a group and emits that group flat so control-flow constructs like
`return` still affect setup() itself.

IIFEUnsafeStatement.__str__ routes its inner through statement() so
bare Expression subclasses pick up the terminating semicolon. Reported
by @swoboda1337.
2026-04-17 18:12:14 -05:00
J. Nick Koston
e26ce59797 for progmem 2026-04-17 16:18:39 -05:00
J. Nick Koston
9fa6d224c2 [core] Tighten docstrings and inline comments 2026-04-17 15:35:53 -05:00
J. Nick Koston
91b238aa97 [core] Fix grammar in ComponentMarker docstring 2026-04-17 15:34:15 -05:00
J. Nick Koston
00f08ba6ed [core] Drop per-component begin/end labels from generated main.cpp
The labels were there to help humans scanning the generated main.cpp
find component boundaries, but they were:

- Unreliable: CORE.flush_tasks can interleave coroutines on each
  await, so a component's later statements can land in another
  component's begin/end block.
- Load-bearing for a pile of complexity: a tuple return from
  _wrap_in_iifes, a has_iife flag, a comment-only detector to
  suppress trailing end-markers for comment-only components, and
  a brittle `"[]()" in line` check that could false-positive on
  YAML dumps containing lambda syntax.
- Not actually needed — generated main.cpp is a build artifact
  rarely read by anyone, and cg.LineComment("name:") already puts
  the component name at the start of its block.

ComponentMarker stays as a pure chunking sentinel — it tells
cpp_main_section where component boundaries are (for grouping) but
produces no C++ output. _wrap_in_iifes returns a plain list again.
Added a regression test for the now-defused case of a comment
containing "[]()" that was previously flagged by review.
2026-04-17 15:19:48 -05:00
J. Nick Koston
f82401a504 [core] Address Copilot review: robust brace depth, accurate docstrings
- Count { and } characters per line instead of matching whole-line
  tokens. Current codegen only emits scope braces as standalone lines
  (from cg.with_local_variable()), but the defensive change is robust
  against future codegen emitting inline control flow like
  `if (cond) {` or `} else {` on one line.
- Add a regression test covering those inline-brace patterns.
- Fix stale docstrings on ComponentMarker and cpp_main_section that
  still claimed "stack frame released on return" and described the
  IIFEs as "noinline". The IIFEs have no noinline attribute and rely
  on scope-based lifetime shortening rather than guaranteed frames.
2026-04-17 15:06:42 -05:00
J. Nick Koston
178f23a7aa [core] Use begin/end marker pairs around each component's IIFE
Rename the bracket markers from "// === X ===" (same on both sides)
to "// === begin X ===" and "// === end X ===" so the generated
main.cpp reads unambiguously when scanning by component. Comment-only
components still get a single "begin X" marker since they have no
IIFE to close.
2026-04-17 15:06:42 -05:00
J. Nick Koston
864d31aa65 [core] Put ComponentMarker outside the IIFE as a visual bracket
The marker comment was being emitted as the first line *inside* each
IIFE:

  []() {
    // === logger ===
    // logger:
    //   ...
    ...
  }();

That works but buries the component label inside the lambda body, so
scanning generated main.cpp to find "where does component X's setup
live" is harder than it needs to be. Emit the marker before and after
the IIFE instead:

  // === logger ===
  []() {
    // logger:
    //   ...
    ...
  }();
  // === logger ===

Comment-only components (e.g. sha256, async_tcp, empty platforms like
binary_sensor:) don't grow a useless trailing duplicate marker —
when there's no IIFE to bracket, the marker is emitted once.
2026-04-17 15:06:42 -05:00
J. Nick Koston
936694af2c [core] Don't emit IIFE for comment-only chunks
Some components (sha256, async_tcp, network, empty text_sensor:, etc.)
emit only a ComponentMarker plus config-dump comments and no actual
C++ statements. Wrapping those in a `[]() { ... }();` IIFE is pure
clutter in the generated main.cpp — the IIFE has no body.

When _wrap_in_iifes sees a chunk whose lines are all // comments,
emit them verbatim instead of wrapping. Peak stack and flash are
unchanged on apollo and neargaragedoor since GCC was already
eliding the empty IIFEs; this just makes the generated code read
cleanly to humans.
2026-04-17 15:06:42 -05:00
J. Nick Koston
6a7c9af870 [core] Drop noinline from IIFE chunks and rename helper
Additional measurements showed GCC's -Os inliner re-inlines most IIFE
chunks back into setup() by choice, and the structural scoping alone
captures nearly all of the peak-stack benefit on esp32 without the
flash cost of forcing all chunks to stay as real functions.

Apollo (esp32-s3, -Os) with vs without noinline:
  peak setup stack     176 B (noinline)  vs  304 B (scope-only)
  flash delta         +388 B (noinline)  vs   -504 B (scope-only)
  chunks kept          86               vs    20

Issue #15796 is an LVGL-setup class of bug that has only surfaced on
esp32 after years in the field; the extra guarantee that noinline
provides is not worth the flash cost in practice. Also rename the
helper from _wrap_in_noinline_iifes to _wrap_in_iifes to match.
2026-04-17 15:06:42 -05:00
J. Nick Koston
29dcf9fc51 [core] Use __attribute__((noinline)) on IIFE lambdas to honor attribute
The C++ standard-attribute spelling [[gnu::noinline]] placed between a
lambda's parameter list and body binds to the return type, not the
call operator. GCC 14 silently ignores it and emits -Wattributes
warnings at every chunk site. Switch to GCC's __attribute__((...))
syntax which binds to operator() as intended.

Measured impact on apollo-r-pro-1-eth (esp32-s3, -Os) vs the broken
[[gnu::noinline]] version: setup() frame 160 B -> 32 B, peak stack
304 B -> 176 B (another -42%). Flash grows by 888 B because all 86
chunks now stay as separate functions instead of GCC inlining the
small ones (which it was free to do when the attribute was ignored).

Net vs baseline -Os: peak stack 1264 B -> 176 B (-86%); flash
+388 B (<0.05% of a typical esp32 partition).
2026-04-17 15:06:42 -05:00
J. Nick Koston
6b67224286 [core] Chunk setup() into per-component noinline IIFEs
Generated setup() is a single monolithic function whose stack frame
scales super-linearly with config size. On a 5,943-line apollo build
the frame reached 1,264 B at -Os; extrapolation onto larger configs
(e.g. the 16k-line LVGL config in #15796) plausibly overflows the
8 KB loop task stack before safe_mode can increment its boot counter.

Emit a ComponentMarker sentinel at the start of each component's
to_code output, then have cpp_main_section wrap each component's
block (and sub-splits of up to 50 statements within each block) in a
noinline IIFE lambda. Each lambda's ENTRY frame is released on
return, bounding peak stack to setup() frame + max chunk frame.

Measured on apollo-r-pro-1-eth (esp32-s3, -Os):

  setup() frame        1264 B  ->  160 B
  max chunk frame      n/a     ->  144 B
  peak setup stack     1264 B  ->  304 B  (-76%)
  total flash      792,471 B   ->  791,995 B  (-476 B)

The brace-depth guard in _wrap_in_noinline_iifes ensures we never
split between the RawStatement("{") / RawStatement("}") pair emitted
by cg.with_local_variable() (currently only wifi), so scoped locals
stay intact.
2026-04-17 15:06:41 -05:00
133 changed files with 1556 additions and 3661 deletions

View File

@@ -1 +1 @@
c65f1a0804a7765462d570c50891ac719260592df2c9cdfe88233fc346ac59e9
075ed2142432dc59883bb52db8ac11270f952851d6400deae080f5468c7cb592

View File

@@ -339,7 +339,7 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30 # v4
with:
run: ${{ steps.build.outputs.binary }}
mode: simulation

View File

@@ -58,7 +58,6 @@ repos:
entry: python3 script/run-in-env.py pylint
language: system
types: [python]
files: ^esphome/.+\.py$
- id: clang-tidy-hash
name: Update clang-tidy hash
entry: python script/clang_tidy_hash.py --update-if-changed

View File

@@ -569,6 +569,7 @@ def wrap_to_code(name, comp):
@functools.wraps(comp.to_code)
async def wrapped(conf):
cg.add(cg.ComponentMarker(name))
cg.add(cg.LineComment(f"{name}:"))
if comp.config_schema is not None:
conf_str = yaml_util.dump(conf)

View File

@@ -10,8 +10,10 @@
# pylint: disable=unused-import
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
ComponentMarker,
Expression,
FlashStringLiteral,
IIFEUnsafeStatement,
LineComment,
LogStringLiteral,
MockObj,

View File

@@ -2,8 +2,6 @@
#include <cstdio>
#include <cstring>
#include "esphome/core/alloc_helpers.h"
namespace esphome {
namespace anova {
@@ -107,14 +105,14 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) {
}
case READ_TARGET_TEMPERATURE:
case SET_TARGET_TEMPERATURE: {
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
if (this->fahrenheit_)
this->target_temp_ = ftoc(this->target_temp_);
this->has_target_temp_ = true;
break;
}
case READ_CURRENT_TEMPERATURE: {
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
if (this->fahrenheit_)
this->current_temp_ = ftoc(this->current_temp_);
this->has_current_temp_ = true;

View File

@@ -291,12 +291,12 @@ CONFIG_SCHEMA = cv.All(
cv.SplitDefault(
CONF_MAX_CONNECTIONS,
esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes
esp32=5, # 520KB RAM available
esp32=8, # 520KB RAM available
rp2040=4, # 264KB RAM but LWIP constraints
bk72xx=5, # Moderate RAM
rtl87xx=5, # Moderate RAM
bk72xx=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
host=8, # Abundant resources
ln882x=5, # Moderate RAM
ln882x=8, # Moderate RAM
): cv.int_range(min=1, max=20),
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
@@ -336,7 +336,8 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
if CONF_LISTEN_BACKLOG in config:
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
if CONF_MAX_CONNECTIONS in config:
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled

View File

@@ -118,7 +118,7 @@ void APIServer::loop() {
this->accept_new_connections_();
}
if (this->api_connection_count_ == 0) {
if (this->clients_.empty()) {
// Check reboot timeout - done in loop to avoid scheduler heap churn
// (cancelled scheduler items sit in heap memory until their scheduled time)
if (this->reboot_timeout_ != 0) {
@@ -135,15 +135,15 @@ void APIServer::loop() {
// Check network connectivity once for all clients
if (!network::is_connected()) {
// Network is down - disconnect all clients
for (auto &client : this->active_clients()) {
for (auto &client : this->clients_) {
client->on_fatal_error();
client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
}
// Continue to process and clean up the clients below
}
uint8_t client_index = 0;
while (client_index < this->api_connection_count_) {
size_t client_index = 0;
while (client_index < this->clients_.size()) {
auto &client = this->clients_[client_index];
// Common case: process active client
@@ -161,7 +161,7 @@ void APIServer::loop() {
}
}
void APIServer::remove_client_(uint8_t client_index) {
void APIServer::remove_client_(size_t client_index) {
auto &client = this->clients_[client_index];
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -179,17 +179,14 @@ void APIServer::remove_client_(uint8_t client_index) {
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap-and-reset: move the removed client to the trailing slot and null it out so slots
// [api_connection_count_, N) remain nullptr.
const uint8_t last_index = this->api_connection_count_ - 1;
if (client_index < last_index) {
std::swap(this->clients_[client_index], this->clients_[last_index]);
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_[last_index].reset();
this->api_connection_count_--;
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) {
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning(LOG_STR("waiting for client connection"));
this->last_connected_ = App.get_loop_component_start_time();
}
@@ -213,8 +210,8 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->api_connection_count_ >= MAX_API_CONNECTIONS) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
@@ -223,11 +220,11 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
ESP_LOGD(TAG, "Accept %s", peername);
auto *conn = new APIConnection(std::move(sock), this);
this->clients_[this->api_connection_count_++].reset(conn);
this->clients_.emplace_back(conn);
conn->start();
// First client connected - clear warning and update timestamp
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0) {
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
@@ -240,7 +237,7 @@ void APIServer::dump_config() {
" Address: %s:%u\n"
" Listen backlog: %u\n"
" Max connections: %u",
network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
#ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
if (!this->noise_ctx_.has_psk()) {
@@ -258,7 +255,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->active_clients()) { \
for (auto &c : this->clients_) { \
if (c->flags_.state_subscription) \
c->send_##entity_name##_state(obj); \
} \
@@ -340,7 +337,7 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal())
return;
for (auto &c : this->active_clients()) {
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_event(obj);
}
@@ -352,7 +349,7 @@ void APIServer::on_event(event::Event *obj) {
void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->active_clients()) {
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_update_state(obj);
}
@@ -363,7 +360,7 @@ void APIServer::on_update(update::UpdateEntity *obj) {
void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
// We could add code to manage a second subscription type, but, since this message type is
// very infrequent and small, we simply send it to all clients
for (auto &c : this->active_clients())
for (auto &c : this->clients_)
c->send_message(msg);
}
#endif
@@ -378,7 +375,7 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
resp.key = key;
resp.timings = timings;
for (auto &c : this->active_clients())
for (auto &c : this->clients_)
c->send_infrared_rf_receive_event(resp);
}
#endif
@@ -395,7 +392,7 @@ void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = bat
#ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
for (auto &client : this->active_clients()) {
for (auto &client : this->clients_) {
client->send_homeassistant_action(call);
}
}
@@ -535,7 +532,7 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
return;
}
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
for (auto &c : this->active_clients()) {
for (auto &c : this->clients_) {
DisconnectRequest req;
c->send_message(req);
}
@@ -586,7 +583,7 @@ bool APIServer::clear_noise_psk(bool make_active) {
#ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() {
for (auto &client : this->active_clients()) {
for (auto &client : this->clients_) {
if (!client->flags_.remove && client->is_authenticated()) {
client->send_time_request();
return; // Only request from one client to avoid clock conflicts
@@ -596,8 +593,8 @@ void APIServer::request_time() {
#endif
bool APIServer::is_connected_with_state_subscription() const {
for (uint8_t i = 0; i < this->api_connection_count_; i++) {
if (this->clients_[i]->flags_.state_subscription) {
for (const auto &client : this->clients_) {
if (client->flags_.state_subscription) {
return true;
}
}
@@ -612,7 +609,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
// we would be filling a buffer we are trying to clear
return;
}
for (auto &c : this->active_clients()) {
for (auto &c : this->clients_) {
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
c->try_send_log_message(level, tag, message, message_len);
}
@@ -621,7 +618,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
#ifdef USE_CAMERA
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
for (auto &c : this->active_clients()) {
for (auto &c : this->clients_) {
if (!c->flags_.remove)
c->set_camera_state(image);
}
@@ -638,7 +635,7 @@ void APIServer::on_shutdown() {
this->batch_delay_ = 5;
// Send disconnect requests to all connected clients
for (auto &c : this->active_clients()) {
for (auto &c : this->clients_) {
DisconnectRequest req;
if (!c->send_message(req)) {
// If we can't send the disconnect request directly (tx_buffer full),
@@ -656,7 +653,7 @@ bool APIServer::teardown() {
this->loop();
// Return true only when all clients have been torn down
return this->api_connection_count_ == 0;
return this->clients_.empty();
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES

View File

@@ -21,8 +21,6 @@
#include "esphome/components/camera/camera.h"
#endif
#include <array>
#include <memory>
#include <vector>
namespace esphome::api {
@@ -65,6 +63,7 @@ class APIServer final : public Component,
void set_batch_delay(uint16_t batch_delay);
uint16_t get_batch_delay() const { return batch_delay_; }
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
// Get reference to shared buffer for API connections
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
@@ -187,26 +186,9 @@ class APIServer final : public Component,
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
#endif
bool is_connected() const { return this->api_connection_count_ != 0; }
bool is_connected() const { return !this->clients_.empty(); }
bool is_connected_with_state_subscription() const;
// Range-for view over the populated slice [0, api_connection_count_). Read-only with respect
// to ownership — callers get `const unique_ptr&` so they can invoke non-const methods on the
// APIConnection but cannot reset/move the slot and break the count invariant.
using APIConnectionPtr = std::unique_ptr<APIConnection>;
class ActiveClientsView {
const APIConnectionPtr *begin_;
const APIConnectionPtr *end_;
public:
ActiveClientsView(const APIConnectionPtr *b, const APIConnectionPtr *e) : begin_(b), end_(e) {}
const APIConnectionPtr *begin() const { return this->begin_; }
const APIConnectionPtr *end() const { return this->end_; }
};
ActiveClientsView active_clients() const {
return {this->clients_.data(), this->clients_.data() + this->api_connection_count_};
}
#ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription {
const char *entity_id; // Pointer to flash (internal) or heap (external)
@@ -252,8 +234,8 @@ class APIServer final : public Component,
protected:
// Accept incoming socket connections. Only called when socket has pending connections.
void __attribute__((noinline)) accept_new_connections_();
// Remove a disconnected client by index. Swaps with the last populated slot and resets it.
void __attribute__((noinline)) remove_client_(uint8_t client_index);
// Remove a disconnected client by index. Swaps with last element and pops.
void __attribute__((noinline)) remove_client_(size_t client_index);
#ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
@@ -291,9 +273,8 @@ class APIServer final : public Component,
uint32_t reboot_timeout_{300000};
uint32_t last_connected_{0};
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
std::array<std::unique_ptr<APIConnection>, MAX_API_CONNECTIONS> clients_{};
// Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_;
// Shared proto write buffer for all connections.
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
@@ -328,10 +309,10 @@ class APIServer final : public Component,
uint16_t port_{6053};
uint16_t batch_delay_{100};
// Connection limits - these defaults will be overridden by config values
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
// from cv.SplitDefault in __init__.py which sets platform-specific defaults
uint8_t listen_backlog_{4};
uint8_t max_connections_{8};
bool shutting_down_ = false;
uint8_t api_connection_count_{0};
// 7 bytes used, 1 byte padding
#ifdef USE_API_NOISE

View File

@@ -20,77 +20,58 @@ constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data)
}
void BL0906::loop() {
while (this->available())
this->flush();
if (this->current_stage_ == STAGE_IDLE) {
// Woken up between cycles to drain the action queue. Go back to sleep.
this->handle_actions_();
this->disable_loop();
if (this->current_channel_ == UINT8_MAX) {
return;
}
if (this->current_stage_ == STAGE_TEMP) {
while (this->available())
this->flush();
if (this->current_channel_ == 0) {
// Temperature
this->read_data_(BL0906_TEMPERATURE, BL0906_TREF, this->temperature_sensor_);
} else if (this->current_stage_ == STAGE_CHANNEL_1) {
} else if (this->current_channel_ == 1) {
this->read_data_(BL0906_I_1_RMS, BL0906_IREF, this->current_1_sensor_);
this->read_data_(BL0906_WATT_1, BL0906_PREF, this->power_1_sensor_);
this->read_data_(BL0906_CF_1_CNT, BL0906_EREF, this->energy_1_sensor_);
} else if (this->current_stage_ == STAGE_CHANNEL_2) {
} else if (this->current_channel_ == 2) {
this->read_data_(BL0906_I_2_RMS, BL0906_IREF, this->current_2_sensor_);
this->read_data_(BL0906_WATT_2, BL0906_PREF, this->power_2_sensor_);
this->read_data_(BL0906_CF_2_CNT, BL0906_EREF, this->energy_2_sensor_);
} else if (this->current_stage_ == STAGE_CHANNEL_3) {
} else if (this->current_channel_ == 3) {
this->read_data_(BL0906_I_3_RMS, BL0906_IREF, this->current_3_sensor_);
this->read_data_(BL0906_WATT_3, BL0906_PREF, this->power_3_sensor_);
this->read_data_(BL0906_CF_3_CNT, BL0906_EREF, this->energy_3_sensor_);
} else if (this->current_stage_ == STAGE_CHANNEL_4) {
} else if (this->current_channel_ == 4) {
this->read_data_(BL0906_I_4_RMS, BL0906_IREF, this->current_4_sensor_);
this->read_data_(BL0906_WATT_4, BL0906_PREF, this->power_4_sensor_);
this->read_data_(BL0906_CF_4_CNT, BL0906_EREF, this->energy_4_sensor_);
} else if (this->current_stage_ == STAGE_CHANNEL_5) {
} else if (this->current_channel_ == 5) {
this->read_data_(BL0906_I_5_RMS, BL0906_IREF, this->current_5_sensor_);
this->read_data_(BL0906_WATT_5, BL0906_PREF, this->power_5_sensor_);
this->read_data_(BL0906_CF_5_CNT, BL0906_EREF, this->energy_5_sensor_);
} else if (this->current_stage_ == STAGE_CHANNEL_6) {
} else if (this->current_channel_ == 6) {
this->read_data_(BL0906_I_6_RMS, BL0906_IREF, this->current_6_sensor_);
this->read_data_(BL0906_WATT_6, BL0906_PREF, this->power_6_sensor_);
this->read_data_(BL0906_CF_6_CNT, BL0906_EREF, this->energy_6_sensor_);
} else if (this->current_stage_ == STAGE_FREQ) {
} else if (this->current_channel_ == UINT8_MAX - 2) {
// Frequency
this->read_data_(BL0906_FREQUENCY, BL0906_FREF, this->frequency_sensor_);
this->read_data_(BL0906_FREQUENCY, BL0906_FREF, frequency_sensor_);
// Voltage
this->read_data_(BL0906_V_RMS, BL0906_UREF, this->voltage_sensor_);
} else if (this->current_stage_ == STAGE_POWER) {
this->read_data_(BL0906_V_RMS, BL0906_UREF, voltage_sensor_);
} else if (this->current_channel_ == UINT8_MAX - 1) {
// Total power
this->read_data_(BL0906_WATT_SUM, BL0906_WATT, this->total_power_sensor_);
// Total Energy
this->read_data_(BL0906_CF_SUM_CNT, BL0906_CF, this->total_energy_sensor_);
} else {
this->current_channel_ = UINT8_MAX - 2; // Go to frequency and voltage
return;
}
this->advance_stage_();
this->current_channel_++;
this->handle_actions_();
}
void BL0906::advance_stage_() {
switch (this->current_stage_) {
case STAGE_CHANNEL_6:
this->current_stage_ = STAGE_FREQ;
break;
case STAGE_FREQ:
this->current_stage_ = STAGE_POWER;
break;
case STAGE_POWER:
// Cycle complete; sleep until the next update().
this->current_stage_ = STAGE_IDLE;
this->disable_loop();
break;
default:
this->current_stage_ = static_cast<BL0906Stage>(this->current_stage_ + 1);
break;
}
}
void BL0906::setup() {
while (this->available())
this->flush();
@@ -104,20 +85,12 @@ void BL0906::setup() {
this->bias_correction_(BL0906_RMSOS_6, 0.01200, 0); // Calibration current_6
this->write_array(USR_WRPROT_ONLYREAD, sizeof(USR_WRPROT_ONLYREAD));
// Loop stays idle until the first update() or enqueued action.
this->disable_loop();
}
void BL0906::update() {
this->current_stage_ = STAGE_TEMP;
this->enable_loop();
}
void BL0906::update() { this->current_channel_ = 0; }
size_t BL0906::enqueue_action_(ActionCallbackFuncPtr function) {
this->action_queue_.push_back(function);
// Ensure the queue is serviced even if the read cycle has already completed.
this->enable_loop();
return this->action_queue_.size();
}

View File

@@ -12,22 +12,6 @@
namespace esphome {
namespace bl0906 {
// Stage values for the read state machine. After STAGE_CHANNEL_6 the state machine
// jumps to the two sentinel stages below, then to STAGE_IDLE which marks the cycle
// as complete and disables the loop.
enum BL0906Stage : uint8_t {
STAGE_TEMP = 0, // chip temperature
STAGE_CHANNEL_1 = 1, // per-phase current + power + energy
STAGE_CHANNEL_2 = 2,
STAGE_CHANNEL_3 = 3,
STAGE_CHANNEL_4 = 4,
STAGE_CHANNEL_5 = 5,
STAGE_CHANNEL_6 = 6,
STAGE_FREQ = UINT8_MAX - 2, // frequency + voltage
STAGE_POWER = UINT8_MAX - 1, // total power + total energy
STAGE_IDLE = UINT8_MAX, // cycle complete
};
struct DataPacket { // NOLINT(altera-struct-pack-align)
uint8_t l{0};
uint8_t m{0};
@@ -95,8 +79,7 @@ class BL0906 : public PollingComponent, public uart::UARTDevice {
void bias_correction_(uint8_t address, float measurements, float correction);
BL0906Stage current_stage_{STAGE_IDLE};
void advance_stage_();
uint8_t current_channel_{0};
size_t enqueue_action_(ActionCallbackFuncPtr function);
void handle_actions_();

View File

@@ -6,7 +6,6 @@ from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Fr
CODEOWNERS = ["@trvrnrth"]
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensor", "text_sensor"]
CONFLICTS_WITH = ["bme68x_bsec2"]
MULTI_CONF = True
CONF_BME680_BSEC_ID = "bme680_bsec_id"

View File

@@ -13,7 +13,6 @@ from esphome.const import (
)
CODEOWNERS = ["@neffs", "@kbx81"]
CONFLICTS_WITH = ["bme680_bsec"]
DOMAIN = "bme68x_bsec2"

View File

@@ -30,7 +30,7 @@ void DebugComponent::dump_config() {
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION);
size_t pos = buf_append_str(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, ESPHOME_VERSION);
size_t pos = buf_append_printf(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION);
this->free_heap_ = get_free_heap_();
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);

View File

@@ -224,21 +224,17 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *model = ESPHOME_VARIANT;
// Build features string
pos = buf_append_str(buf, size, pos, "|Chip: ");
pos = buf_append_str(buf, size, pos, model);
pos = buf_append_str(buf, size, pos, " Features:");
pos = buf_append_printf(buf, size, pos, "|Chip: %s Features:", model);
bool first_feature = true;
for (const auto &feature : CHIP_FEATURES) {
if (info.features & feature.bit) {
pos = buf_append_str(buf, size, pos, first_feature ? "" : ", ");
pos = buf_append_str(buf, size, pos, feature.name);
pos = buf_append_printf(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name);
first_feature = false;
info.features &= ~feature.bit;
}
}
if (info.features != 0) {
pos = buf_append_str(buf, size, pos, first_feature ? "" : ", ");
pos = buf_append_printf(buf, size, pos, "Other:0x%" PRIx32, info.features);
pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features);
}
pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision);
@@ -271,20 +267,17 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
// Framework detection
#ifdef USE_ARDUINO
ESP_LOGD(TAG, " Framework: Arduino");
pos = buf_append_str(buf, size, pos, "|Framework: Arduino");
pos = buf_append_printf(buf, size, pos, "|Framework: Arduino");
#else
ESP_LOGD(TAG, " Framework: ESP-IDF");
pos = buf_append_str(buf, size, pos, "|Framework: ESP-IDF");
pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF");
#endif
pos = buf_append_str(buf, size, pos, "|ESP-IDF: ");
pos = buf_append_str(buf, size, pos, esp_get_idf_version());
pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3],
mac[4], mac[5]);
pos = buf_append_str(buf, size, pos, "|Reset: ");
pos = buf_append_str(buf, size, pos, reset_reason);
pos = buf_append_str(buf, size, pos, "|Wakeup: ");
pos = buf_append_str(buf, size, pos, wakeup_cause);
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause);
return pos;
}

View File

@@ -38,12 +38,9 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id,
lt_get_board_code(), flash_kib, ram_kib, reset_reason);
pos = buf_append_str(buf, size, pos, "|Version: ");
pos = buf_append_str(buf, size, pos, LT_BANNER_STR + 10);
pos = buf_append_str(buf, size, pos, "|Reset Reason: ");
pos = buf_append_str(buf, size, pos, reset_reason);
pos = buf_append_str(buf, size, pos, "|Chip Name: ");
pos = buf_append_str(buf, size, pos, lt_cpu_get_model_name());
pos = buf_append_printf(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10);
pos = buf_append_printf(buf, size, pos, "|Reset Reason: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name());
pos = buf_append_printf(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id);
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib);
pos = buf_append_printf(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib);

View File

@@ -162,18 +162,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *supply_status =
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
ESP_LOGD(TAG, "Main supply status: %s", supply_status);
pos = buf_append_str(buf, size, pos, "|Main supply status: ");
pos = buf_append_str(buf, size, pos, supply_status);
pos = buf_append_printf(buf, size, pos, "|Main supply status: %s", supply_status);
// Regulator stage 0
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
const char *reg0_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
const char *reg0_voltage = regout0_to_str((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos);
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append_str(buf, size, pos, "|Regulator stage 0: ");
pos = buf_append_str(buf, size, pos, reg0_type);
pos = buf_append_str(buf, size, pos, ", ");
pos = buf_append_str(buf, size, pos, reg0_voltage);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
#ifdef USE_NRF52_REG0_VOUT
if ((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos != USE_NRF52_REG0_VOUT) {
ESP_LOGE(TAG, "Regulator stage 0: expected %s", regout0_to_str(USE_NRF52_REG0_VOUT));
@@ -181,14 +177,13 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
#endif
} else {
ESP_LOGD(TAG, "Regulator stage 0: disabled");
pos = buf_append_str(buf, size, pos, "|Regulator stage 0: disabled");
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled");
}
// Regulator stage 1
const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type);
pos = buf_append_str(buf, size, pos, "|Regulator stage 1: ");
pos = buf_append_str(buf, size, pos, reg1_type);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 1: %s", reg1_type);
// USB power state
const char *usb_state;
@@ -202,8 +197,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
usb_state = "disconnected";
}
ESP_LOGD(TAG, "USB power state: %s", usb_state);
pos = buf_append_str(buf, size, pos, "|USB power state: ");
pos = buf_append_str(buf, size, pos, usb_state);
pos = buf_append_printf(buf, size, pos, "|USB power state: %s", usb_state);
// Power-fail comparator
bool enabled;
@@ -308,18 +302,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
break;
}
ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
pos = buf_append_str(buf, size, pos, "|Power-fail comparator: ");
pos = buf_append_str(buf, size, pos, pof_voltage);
pos = buf_append_str(buf, size, pos, ", VDDH: ");
pos = buf_append_str(buf, size, pos, vddh_voltage);
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
} else {
ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage);
pos = buf_append_str(buf, size, pos, "|Power-fail comparator: ");
pos = buf_append_str(buf, size, pos, pof_voltage);
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s", pof_voltage);
}
} else {
ESP_LOGD(TAG, "Power-fail comparator: disabled");
pos = buf_append_str(buf, size, pos, "|Power-fail comparator: disabled");
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: disabled");
}
auto package = [](uint32_t value) {

View File

@@ -1,97 +0,0 @@
#include "epaper_spi_ssd1683.h"
#include <algorithm>
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.mono";
void EPaperSSD1683::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh screen");
this->cmd_data(0x3C, {partial ? (uint8_t) 0x80 : (uint8_t) 0x01});
// On partial update, set red RAM to inverse to remove BW ghosting
this->cmd_data(0x21, {partial ? (uint8_t) 0x80 : (uint8_t) 0x40, (uint8_t) 0x00});
// Set full update to 0xD7 for fast update, 0xF7 for normal
// Fast update flashes less and draws sooner but is in busy state for the same amount of time
// Manufacturer recommends not using fast update all the time, TODO expose this to the user
this->cmd_data(0x22, {partial ? (uint8_t) 0xFC : (uint8_t) 0xF7});
this->command(0x20);
}
// Puts the display into deep sleep mode 1, only way to get out is to reset the display
// Mode 1 retains RAM while sleeping, necessary for future partial and window updates
void EPaperSSD1683::deep_sleep() {
if (this->is_using_partial_update_()) {
ESP_LOGV(TAG, "Deep sleep mode 1");
this->cmd_data(0x10, {0x01}); // deep sleep, retain RAM
} else {
ESP_LOGV(TAG, "Deep sleep mode 2");
this->cmd_data(0x10, {0x03}); // deep sleep, lose RAM
}
}
void EPaperSSD1683::set_window() {
// if not using partial update, the display will go into deep sleep mode 2, so must rewrite entire
// buffer since the display RAM will not retain contents
if (!this->is_using_partial_update_()) {
this->x_low_ = 0;
this->x_high_ = this->width_;
this->y_low_ = 0;
this->y_high_ = this->height_;
}
// round x-coordinates to byte boundaries
this->x_low_ /= 8;
this->x_high_ += 7;
this->x_high_ /= 8;
this->cmd_data(0x44, {(uint8_t) this->x_low_, (uint8_t) (this->x_high_ - 1)});
this->cmd_data(0x45, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256), (uint8_t) (this->y_high_ - 1),
(uint8_t) ((this->y_high_ - 1) / 256)});
this->cmd_data(0x4E, {(uint8_t) this->x_low_});
this->cmd_data(0x4F, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256)});
}
bool HOT EPaperSSD1683::transfer_data() {
auto start_time = millis();
if (this->current_data_index_ == 0) {
if (this->send_red_) {
// round to byte boundaries
this->set_window();
}
// for monochrome, we need to send red on every refresh to prevent dirty pixels
// when doing a partial refresh
this->command(this->send_red_ ? 0x26 : 0x24);
this->current_data_index_ = this->y_low_; // actually current line
}
size_t row_length = this->x_high_ - this->x_low_;
FixedVector<uint8_t> bytes_to_send{};
bytes_to_send.init(row_length);
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
this->start_data_();
while (this->current_data_index_ != this->y_high_) {
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_;
for (size_t i = 0; i != row_length; i++) {
bytes_to_send[i] = this->buffer_[data_idx++];
}
++this->current_data_index_;
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
this->disable();
return false;
}
}
this->disable();
this->current_data_index_ = 0;
if (this->send_red_) {
this->send_red_ = false;
return false;
}
this->send_red_ = true;
return true;
}
} // namespace esphome::epaper_spi

View File

@@ -1,22 +0,0 @@
#pragma once
#include "epaper_spi_mono.h"
namespace esphome::epaper_spi {
/**
* A class for Solomon SSD1683 epaper displays.
*/
class EPaperSSD1683 : public EPaperMono {
public:
EPaperSSD1683(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperMono(name, width, height, init_sequence, init_sequence_length) {}
protected:
void refresh_screen(bool partial) override;
void deep_sleep() override;
void set_window() override;
bool transfer_data() override;
};
} // namespace esphome::epaper_spi

View File

@@ -1,27 +0,0 @@
from esphome.const import CONF_DATA_RATE
from . import EpaperModel
class SSD1683(EpaperModel):
def __init__(self, name, class_name="EPaperSSD1683", data_rate="20MHz", **defaults):
defaults[CONF_DATA_RATE] = data_rate
super().__init__(name, class_name, **defaults)
# fmt: off
def get_init_sequence(self, config: dict):
_width, height = self.get_dimensions(config)
return (
(0x01, (height - 1) % 256, (height - 1) // 256, 0x00), # Set column gate limit
(0x18, 0x80), # Select internal Temp sensor
(0x11, 0x03), # Set transform
)
ssd1683 = SSD1683("ssd1683")
goodisplay_gdey042t81 = ssd1683.extend(
"goodisplay-gdey042t81-4.2",
width=400,
height=300,
)

View File

@@ -128,30 +128,23 @@ ASSERTION_LEVELS = {
SIGNING_SCHEMES = {
"rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME",
"ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME",
"ecdsa_v1": "CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME",
}
# Chip variants that only support one V2 signing scheme.
# Chip variants that only support one signing scheme for Secure Boot V2.
# Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h.
# Variants not listed in either set support both RSA and ECDSA V2
# Variants not listed in either set support both RSA and ECDSA
# (e.g. C5, C6, H2, P4). New variants should be added to the
# appropriate set if they only support one scheme.
# Note: VARIANT_ESP32 is not listed here because it supports V2 RSA only
# when minimum_chip_revision >= 3.0, which requires special handling.
SIGNED_OTA_V2_RSA_ONLY_VARIANTS = {
SIGNED_OTA_RSA_ONLY_VARIANTS = {
VARIANT_ESP32,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C3,
}
SIGNED_OTA_V2_ECC_ONLY_VARIANTS = {
SIGNED_OTA_ECC_ONLY_VARIANTS = {
VARIANT_ESP32C2,
VARIANT_ESP32C61,
}
# V1 ECDSA (Secure Boot V1) is only supported on the original ESP32.
# Based on SOC_SECURE_BOOT_V1 in soc_caps.h.
SIGNED_OTA_V1_ECDSA_VARIANTS = {
VARIANT_ESP32,
}
COMPILER_OPTIMIZATIONS = {
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
@@ -998,73 +991,25 @@ def final_validate(config):
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
scheme = signed_ota[CONF_SIGNING_SCHEME]
variant = config[CONF_VARIANT]
min_rev = advanced.get(CONF_MINIMUM_CHIP_REVISION)
scheme_path = [
CONF_FRAMEWORK,
CONF_ADVANCED,
CONF_SIGNED_OTA_VERIFICATION,
CONF_SIGNING_SCHEME,
]
# V1 ECDSA is only available on the original ESP32
if scheme == "ecdsa_v1" and variant not in SIGNED_OTA_V1_ECDSA_VARIANTS:
scheme_variant_conflicts = {
"ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"),
"rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"),
}
if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[
0
]:
errs.append(
cv.Invalid(
f"Signing scheme 'ecdsa_v1' is only supported on "
f"{VARIANT_FRIENDLY[VARIANT_ESP32]}. "
f"Use 'rsa3072' or 'ecdsa256' instead.",
path=scheme_path,
f"Signing scheme '{scheme}' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
path=[
CONF_FRAMEWORK,
CONF_ADVANCED,
CONF_SIGNED_OTA_VERIFICATION,
CONF_SIGNING_SCHEME,
],
)
)
elif variant == VARIANT_ESP32:
# On ESP32, V2 RSA requires minimum_chip_revision >= 3.0
# Note: string comparison works here because cv.one_of constrains
# min_rev to known ESP32_CHIP_REVISIONS values ("0.0".."3.1").
if scheme == "rsa3072" and (min_rev is None or min_rev < "3.0"):
errs.append(
cv.Invalid(
f"Signing scheme 'rsa3072' on {VARIANT_FRIENDLY[variant]} "
f"requires minimum_chip_revision: '3.0' or higher "
f"(Secure Boot V2 RSA needs chip revision 3.0+). "
f"For older chip revisions, use 'ecdsa_v1' instead.",
path=scheme_path,
)
)
# ESP32 does not support V2 ECDSA (no SOC_SECURE_BOOT_V2_ECC)
elif scheme == "ecdsa256":
errs.append(
cv.Invalid(
f"Signing scheme 'ecdsa256' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use 'rsa3072' (with "
f"minimum_chip_revision: '3.0') or 'ecdsa_v1' instead.",
path=scheme_path,
)
)
# V1 on rev 3.0+ -- suggest V2 RSA for stronger security
elif scheme == "ecdsa_v1" and min_rev is not None and min_rev >= "3.0":
_LOGGER.info(
"Using Secure Boot V1 ECDSA on %s rev %s. "
"Consider using 'rsa3072' (Secure Boot V2 RSA) for "
"stronger security on chip revision 3.0+.",
VARIANT_FRIENDLY[variant],
min_rev,
)
else:
# Non-ESP32 variants: check V2 scheme-variant compatibility
scheme_variant_conflicts = {
"ecdsa256": (SIGNED_OTA_V2_RSA_ONLY_VARIANTS, "rsa3072"),
"rsa3072": (SIGNED_OTA_V2_ECC_ONLY_VARIANTS, "ecdsa256"),
}
if (
conflict := scheme_variant_conflicts.get(scheme)
) and variant in conflict[0]:
errs.append(
cv.Invalid(
f"Signing scheme '{scheme}' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
path=scheme_path,
)
)
if CONF_OTA not in full_config:
_LOGGER.warning(
"Signed OTA verification is enabled but no OTA component is configured. "

View File

@@ -172,16 +172,10 @@ def validate_gpio_pin(pin):
exc,
)
else:
# `ignore_pin_validation_error` only suppresses an error raised by the
# variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin
# numbers). If that didn't raise, the option is a no-op -- warn so the
# user can clean it up, but don't block the build.
# Throw an exception if used for a pin that would not have resulted
# in a validation error anyway!
if ignore_pin_validation_warning:
_LOGGER.warning(
"GPIO%d has no validation errors to ignore; "
"remove `ignore_pin_validation_error: true` from this pin.",
pin[CONF_NUMBER],
)
raise cv.Invalid(f"GPIO{pin[CONF_NUMBER]} is not a reserved pin")
return pin

View File

@@ -5,7 +5,6 @@ import json # noqa: E402
import os # noqa: E402
import pathlib # noqa: E402
import shutil # noqa: E402
import subprocess # noqa: E402
from glob import glob # noqa: E402
@@ -26,114 +25,6 @@ def _parse_sdkconfig(sdkconfig_path):
return options
def _generate_v1_verification_key(env):
"""Generate the V1 ECDSA verification key binary and assembly source file.
Secure Boot V1 embeds the public verification key directly in the app binary
as a compiled object (via a .S assembly file). The ESP-IDF CMake build generates
these files via custom commands, but PlatformIO's SCons bridge does not execute
them. This function replicates that logic:
1. Extracts the raw public key from the PEM signing key using espsecure.
2. Generates the .S assembly source that embeds the key bytes.
"""
build_dir = pathlib.Path(env.subst("$BUILD_DIR"))
project_dir = pathlib.Path(env.subst("$PROJECT_DIR"))
pioenv = env.subst("$PIOENV")
sdkconfig = _parse_sdkconfig(project_dir / f"sdkconfig.{pioenv}")
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") != "y":
return
bin_path = build_dir / "signature_verification_key.bin"
asm_path = build_dir / "signature_verification_key.bin.S"
# Determine the source of the verification key
if sdkconfig.get("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES") == "y":
# Extract public key from the signing key
signing_key = sdkconfig.get("CONFIG_SECURE_BOOT_SIGNING_KEY")
if not signing_key:
return
signing_key_path = pathlib.Path(signing_key)
if not signing_key_path.exists():
print(f"Error: V1 ECDSA signing key not found: {signing_key_path}")
env.Exit(1)
return
if not bin_path.exists() or bin_path.stat().st_mtime < signing_key_path.stat().st_mtime:
python_exe = env.subst("$PYTHONEXE")
result = subprocess.run(
[python_exe, "-m", "espsecure", "extract_public_key",
"--keyfile", str(signing_key_path), str(bin_path)],
capture_output=True, text=True,
)
if result.returncode != 0:
print(f"Error extracting V1 verification key: {result.stderr}")
env.Exit(1)
return
print(f"Extracted V1 ECDSA verification key from {signing_key_path.name}")
else:
# User-provided verification key -- should already be a raw binary file
verification_key = sdkconfig.get("CONFIG_SECURE_BOOT_VERIFICATION_KEY")
if not verification_key:
return
verification_key_path = pathlib.Path(verification_key)
if not verification_key_path.exists():
print(f"Error: Verification key not found: {verification_key_path}")
env.Exit(1)
return
shutil.copyfile(str(verification_key_path), str(bin_path))
if not bin_path.exists():
return
# Generate the .S assembly file from the binary key data.
# Replicates ESP-IDF's data_file_embed_asm.cmake with RENAME_TO=signature_verification_key_bin.
# The file is needed in both the app build dir and the bootloader build dir, since
# the bootloader also embeds the verification key when CONFIG_SECURE_SIGNED_ON_BOOT_NO_SECURE_BOOT
# is enabled. PlatformIO's SCons bridge does not execute the CMake custom commands that
# normally generate these files.
data = bin_path.read_bytes()
varname = "signature_verification_key_bin"
lines = []
lines.append(f"/* Data converted from {bin_path.name} */")
lines.append(".data")
lines.append("#if !defined (__APPLE__) && !defined (__linux__)")
lines.append(".section .rodata.embedded")
lines.append("#endif")
lines.append(f"\n.global {varname}")
lines.append(f"{varname}:")
lines.append(f"\n.global _binary_{varname}_start")
lines.append(f"_binary_{varname}_start: /* for objcopy compatibility */")
# Format binary data as .byte lines (16 bytes per line)
for i in range(0, len(data), 16):
chunk = data[i:i + 16]
hex_bytes = ", ".join(f"0x{b:02x}" for b in chunk)
lines.append(f".byte {hex_bytes}")
lines.append(f"\n.global _binary_{varname}_end")
lines.append(f"_binary_{varname}_end: /* for objcopy compatibility */")
lines.append(f"\n.global {varname}_length")
lines.append(f"{varname}_length:")
lines.append(f".long {len(data)}")
lines.append("")
lines.append('#if defined (__linux__)')
lines.append('.section .note.GNU-stack,"",@progbits')
lines.append("#endif")
asm_content = "\n".join(lines) + "\n"
# Write to app build dir and bootloader build dir
asm_path.write_text(asm_content)
bootloader_dir = build_dir / "bootloader"
if bootloader_dir.is_dir():
bootloader_bin = bootloader_dir / "signature_verification_key.bin"
bootloader_asm = bootloader_dir / "signature_verification_key.bin.S"
shutil.copyfile(str(bin_path), str(bootloader_bin))
bootloader_asm.write_text(asm_content)
def sign_firmware(source, target, env):
"""
Sign the firmware binary using espsecure.py if signed OTA verification is enabled.
@@ -164,12 +55,9 @@ def sign_firmware(source, target, env):
env.Exit(1)
return
# Determine espsecure signature version from the signing scheme:
# V1 ECDSA (Secure Boot V1) uses --version 1, V2 RSA/ECDSA use --version 2.
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") == "y":
sign_version = "1"
else:
sign_version = "2"
# ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes),
# so the espsecure signature version is always 2.
sign_version = "2"
firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin"
firmware_path = build_dir / firmware_name
@@ -329,11 +217,6 @@ def esp32_copy_ota_bin(source, target, env):
print(f"Copied firmware to {new_file_name}")
# Generate V1 ECDSA verification key files before build starts.
# Workaround for PlatformIO not executing CMake custom commands that extract
# the public key and generate the .S assembly file for Secure Boot V1.
_generate_v1_verification_key(env) # noqa: F821
# Run signing first, then merge, then ota copy
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821

View File

@@ -7,7 +7,6 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_USE_PSRAM
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
from esphome.components.esp32.const import VARIANT_ESP32C2
import esphome.config_validation as cv
@@ -343,9 +342,6 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS)
),
cv.Optional(CONF_USE_PSRAM): cv.All(
cv.only_on_esp32, cv.requires_component("psram"), cv.boolean
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -602,22 +598,6 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
# When PSRAM and BT are used together, Bluedroid should prefer SPIRAM for
# heap allocations and use dynamic (heap-based) environment memory tables
# instead of large static DRAM arrays. This frees ~40 kB of internal RAM.
# Reference: Espressif ADF Design Considerations
# https://espressif-docs.readthedocs-hosted.com/projects/esp-adf/en/latest/
# design-guide/design-considerations.html
if config.get(CONF_USE_PSRAM, False):
cg.add_define("USE_ESP32_BLE_PSRAM")
# CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST is only available on ESP32
# (BTDM dual-mode controller). BLE-only SoCs (C3, S3, C2, H2) do not
# expose this Kconfig symbol; applying it there would cause a build error.
if get_esp32_variant() == const.VARIANT_ESP32:
add_idf_sdkconfig_option("CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST", True)
# CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY applies to all Bluedroid-enabled variants.
add_idf_sdkconfig_option("CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY", True)
# Register the core BLE loggers that are always needed
register_bt_logger(BTLoggers.GAP, BTLoggers.BTM, BTLoggers.HCI)

View File

@@ -667,9 +667,6 @@ void ESP32BLE::dump_config() {
" MAC address: %s\n"
" IO Capability: %s",
mac_s, io_capability_s);
#ifdef USE_ESP32_BLE_PSRAM
ESP_LOGCONFIG(TAG, " PSRAM BLE allocation: enabled");
#endif
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
const char *auth_req_mode_s = "<default>";

View File

@@ -22,7 +22,7 @@ void HttpRequestComponent::dump_config() {
}
std::string HttpContainer::get_response_header(const std::string &header_name) {
auto lower = str_lower_case(header_name); // NOLINT
auto lower = str_lower_case(header_name);
for (const auto &entry : this->response_headers_) {
if (entry.name == lower) {
ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str());

View File

@@ -11,7 +11,6 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/alloc_helpers.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -401,7 +400,7 @@ class HttpRequestComponent : public Component {
std::vector<std::string> lower;
lower.reserve(collect_headers.size());
for (const auto &h : collect_headers) {
lower.push_back(str_lower_case(h)); // NOLINT
lower.push_back(str_lower_case(h));
}
return this->perform(url, method, body, request_headers, lower);
}
@@ -416,7 +415,7 @@ class HttpRequestComponent : public Component {
std::vector<std::string> lower;
lower.reserve(collect_headers.size());
for (const auto &h : collect_headers) {
lower.push_back(str_lower_case(h)); // NOLINT
lower.push_back(str_lower_case(h));
}
return this->perform(url, method, body, std::vector<Header>(request_headers.begin(), request_headers.end()), lower);
}

View File

@@ -161,7 +161,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
container->response_headers_.clear();
auto header_count = container->client_.headers();
for (int i = 0; i < header_count; i++) {
const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); // NOLINT
const std::string header_name = str_lower_case(container->client_.headerName(i).c_str());
if (should_collect_header(lower_case_collect_headers, header_name)) {
std::string header_value = container->client_.header(i).c_str();
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());

View File

@@ -115,7 +115,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
container->content_length = container->response_body_.size();
for (auto header : response.headers) {
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
auto lower_name = str_lower_case(header.first); // NOLINT
auto lower_name = str_lower_case(header.first);
if (should_collect_header(lower_case_collect_headers, lower_name)) {
container->response_headers_.push_back({lower_name, header.second});
}

View File

@@ -38,7 +38,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
switch (evt->event_id) {
case HTTP_EVENT_ON_HEADER: {
const std::string header_name = str_lower_case(evt->header_key); // NOLINT
const std::string header_name = str_lower_case(evt->header_key);
if (should_collect_header(user_data->lower_case_collect_headers, header_name)) {
const std::string header_value = evt->header_value;
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());

View File

@@ -756,7 +756,7 @@ async def write_image(config, all_frames=False):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
encoder.end_image()
encoder.end_image()
rhs = [HexInt(x) for x in encoder.data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)

View File

@@ -766,38 +766,32 @@ void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY
void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); }
void LD2412Component::set_basic_config() {
uint8_t min_gate = 1;
uint8_t max_gate = TOTAL_GATES;
uint16_t timeout = DEFAULT_PRESENCE_TIMEOUT;
uint8_t out_pin_level = 0x01;
#ifdef USE_NUMBER
if (this->min_distance_gate_number_ != nullptr) {
if (!this->min_distance_gate_number_->has_state())
return;
min_gate = static_cast<int>(this->min_distance_gate_number_->state);
}
if (this->max_distance_gate_number_ != nullptr) {
if (!this->max_distance_gate_number_->has_state())
return;
max_gate = static_cast<int>(this->max_distance_gate_number_->state) + 1;
}
if (this->timeout_number_ != nullptr) {
if (!this->timeout_number_->has_state())
return;
timeout = static_cast<int>(this->timeout_number_->state);
if (!this->min_distance_gate_number_->has_state() || !this->max_distance_gate_number_->has_state() ||
!this->timeout_number_->has_state()) {
return;
}
#endif
#ifdef USE_SELECT
if (this->out_pin_level_select_ != nullptr) {
if (!this->out_pin_level_select_->has_state())
return;
out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
if (!this->out_pin_level_select_->has_state()) {
return;
}
#endif
uint8_t value[5] = {
lowbyte(min_gate), lowbyte(max_gate), lowbyte(timeout), highbyte(timeout), out_pin_level,
#ifdef USE_NUMBER
lowbyte(static_cast<int>(this->min_distance_gate_number_->state)),
lowbyte(static_cast<int>(this->max_distance_gate_number_->state) + 1),
lowbyte(static_cast<int>(this->timeout_number_->state)),
highbyte(static_cast<int>(this->timeout_number_->state)),
#else
1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0,
#endif
#ifdef USE_SELECT
find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()),
#else
0x01, // Default value if not using select
#endif
};
this->set_config_mode_(true);
this->send_command_(CMD_BASIC_CONF, value, sizeof(value));

View File

@@ -222,7 +222,7 @@ class LightCall {
inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
// Helper to set flag - defaults to true for common case
void set_flag_(FieldFlags flag, bool value = true) ESPHOME_ALWAYS_INLINE {
void set_flag_(FieldFlags flag, bool value = true) {
if (value) {
this->flags_ |= flag;
} else {
@@ -231,7 +231,7 @@ class LightCall {
}
// Helper to clear flag - reduces code size for common case
void clear_flag_(FieldFlags flag) ESPHOME_ALWAYS_INLINE { this->flags_ &= ~flag; }
void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; }
// Helper to log unsupported feature and clear flag - reduces code duplication
void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log);

View File

@@ -35,11 +35,9 @@ LockStateForwarder = lock_ns.class_("LockStateForwarder")
LockState = lock_ns.enum("LockState")
LOCK_STATES = {
"OPEN": LockState.LOCK_STATE_OPEN,
"LOCKED": LockState.LOCK_STATE_LOCKED,
"UNLOCKED": LockState.LOCK_STATE_UNLOCKED,
"JAMMED": LockState.LOCK_STATE_JAMMED,
"OPENING": LockState.LOCK_STATE_OPENING,
"LOCKING": LockState.LOCK_STATE_LOCKING,
"UNLOCKING": LockState.LOCK_STATE_UNLOCKING,
}

View File

@@ -8,10 +8,9 @@ namespace esphome::lock {
static const char *const TAG = "lock";
// Lock state strings indexed by LockState enum.
// Lock state strings indexed by LockState enum (0-5): NONE(UNKNOWN), LOCKED, UNLOCKED, JAMMED, LOCKING, UNLOCKING
// Index 0 is UNKNOWN (for LOCK_STATE_NONE), also used as fallback for out-of-range
PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING", "OPENING",
"OPEN");
PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING");
const LogString *lock_state_to_string(LockState state) {
return LockStateStrings::get_log_str(static_cast<uint8_t>(state), 0);
@@ -75,16 +74,12 @@ LockCall &LockCall::set_state(optional<LockState> state) {
return *this;
}
LockCall &LockCall::set_state(const char *state) {
if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("OPEN")) == 0) {
this->set_state(LOCK_STATE_OPEN);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) {
if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) {
this->set_state(LOCK_STATE_LOCKED);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKED")) == 0) {
this->set_state(LOCK_STATE_UNLOCKED);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("JAMMED")) == 0) {
this->set_state(LOCK_STATE_JAMMED);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("OPENING")) == 0) {
this->set_state(LOCK_STATE_OPENING);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKING")) == 0) {
this->set_state(LOCK_STATE_LOCKING);
} else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKING")) == 0) {

View File

@@ -26,9 +26,7 @@ enum LockState : uint8_t {
LOCK_STATE_UNLOCKED = 2,
LOCK_STATE_JAMMED = 3,
LOCK_STATE_LOCKING = 4,
LOCK_STATE_UNLOCKING = 5,
LOCK_STATE_OPENING = 6,
LOCK_STATE_OPEN = 7,
LOCK_STATE_UNLOCKING = 5
};
const LogString *lock_state_to_string(LockState state);

View File

@@ -45,7 +45,6 @@ optional<uint32_t> LTR390Component::read_sensor_data_(LTR390MODE mode) {
uint8_t buffer[num_bytes];
// Wait until data available
constexpr uint32_t max_wait_ms = 25;
const uint32_t now = millis();
while (true) {
std::bitset<8> status = this->reg(LTR390_MAIN_STATUS).get();
@@ -53,12 +52,12 @@ optional<uint32_t> LTR390Component::read_sensor_data_(LTR390MODE mode) {
if (available)
break;
if (millis() - now > max_wait_ms) {
if (millis() - now > 100) {
ESP_LOGW(TAG, "Sensor didn't return any data, aborting");
return {};
}
ESP_LOGV(TAG, "Waiting for data");
delay(1);
ESP_LOGD(TAG, "Waiting for data");
delay(2);
}
if (!this->read_bytes(MODEADDRESSES[mode], buffer, num_bytes)) {

View File

@@ -89,12 +89,10 @@
id: hello_world_label_
text: "Hello World!"
align: center
- container:
- obj:
id: hello_world_qrcode_
outline_width: 0
border_width: 0
height: 100
width: 100
hidden: !lambda |-
return lv_obj_get_width(lv_screen_active()) < 300 && lv_obj_get_height(lv_screen_active()) < 400;
widgets:

View File

@@ -642,28 +642,26 @@ void LvglComponent::write_random_() {
int iterations = 6 - lv_display_get_inactive_time(this->disp_) / 60000;
if (iterations <= 0)
iterations = 1;
int16_t width = lv_display_get_horizontal_resolution(this->disp_);
int16_t height = lv_display_get_vertical_resolution(this->disp_);
while (iterations-- != 0) {
int32_t col = random_uint32() % width;
int32_t col = random_uint32() % this->width_;
col = col / this->draw_rounding * this->draw_rounding;
int32_t row = random_uint32() % height;
int32_t row = random_uint32() % this->height_;
row = row / this->draw_rounding * this->draw_rounding;
// size will be between 8 and 32, and a multiple of draw_rounding
int32_t size = (random_uint32() % 25 + 8) / this->draw_rounding * this->draw_rounding;
lv_area_t area{.x1 = col, .y1 = row, .x2 = col + size - 1, .y2 = row + size - 1};
lv_area_t area{col, row, col + size - 1, row + size - 1};
// clip to display bounds just in case
if (area.x2 >= width)
area.x2 = width - 1;
if (area.y2 >= height)
area.y2 = height - 1;
if (area.x2 >= this->width_)
area.x2 = this->width_ - 1;
if (area.y2 >= this->height_)
area.y2 = this->height_ - 1;
// line_len can't exceed 1024, and minimum buffer size is 2048, so this won't overflow the buffer
size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2;
for (size_t i = 0; i != line_len; i++) {
reinterpret_cast<uint32_t *>(this->draw_buf_)[i] = random_uint32();
((uint32_t *) (this->draw_buf_))[i] = random_uint32();
}
this->draw_buffer_(&area, reinterpret_cast<lv_color_data *>(this->draw_buf_));
this->draw_buffer_(&area, (lv_color_data *) this->draw_buf_);
}
}

View File

@@ -88,12 +88,6 @@ inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image,
inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
::lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
}
inline void lv_style_set_bg_image_src(lv_style_t *style, image::Image *image) {
::lv_style_set_bg_image_src(style, image->get_lv_image_dsc());
}
inline void lv_style_set_bitmap_mask_src(lv_style_t *style, image::Image *image) {
::lv_style_set_bitmap_mask_src(style, image->get_lv_image_dsc());
}
#endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ANIMIMG
inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) {

View File

@@ -77,11 +77,8 @@ class ArcType(NumberType):
# start_angle and end_angle are mapped to bg_start_angle and bg_end_angle
prop = str(prop)
if prop.endswith("_angle"):
await w.set_property(
"bg_" + prop, await validator.process(config.get(prop))
)
else:
await w.set_property(prop, config, processor=validator)
prop = "bg_" + prop
await w.set_property(prop, config, processor=validator)
if CONF_ADJUSTABLE in config:
if not config[CONF_ADJUSTABLE]:
lv_obj.remove_style(w.obj, nullptr, LV_PART.KNOB)

View File

@@ -52,23 +52,19 @@ class KeyboardType(WidgetType):
if mode := config.get(CONF_MODE):
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode))
if textarea := config.get(CONF_TEXTAREA):
if not is_widget_completed(textarea):
# Can only happen for an initial config, where the keyboard is configured before the
# textarea, so it's ok to always emit into the global context
async def add_textarea():
async with LvContext():
await w.set_property(
CONF_TEXTAREA,
(await get_widgets(config, CONF_TEXTAREA))[0].obj,
)
# If a textarea is configured, it must be generated before the keyboard can attach it.
# If not yet configured, defer the attachment code.
CORE.add_job(add_textarea)
async def add_textarea():
async with LvContext():
await w.set_property(
CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj
)
if is_widget_completed(textarea):
await add_textarea()
else:
# Handles updates in automations, and properly ordered initial config. Code is generated
# into the enclosing context (main or lambda)
await w.set_property(
CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj
)
CORE.add_job(add_textarea)
keyboard_spec = KeyboardType()

View File

@@ -454,12 +454,12 @@ async def to_code(config):
# Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn)
esp32.add_idf_component(name="espressif/esp-nn", ref="1.1.2")
esp32.add_idf_component(name="esphome/esp-micro-speech-features", ref="1.2.3")
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
cg.add_build_flag("-DESP_NN")
cg.add_library("kahrendt/ESPMicroSpeechFeatures", "1.1.0")
if vad_model := config.get(CONF_VAD):
cg.add_define("USE_MICRO_WAKE_WORD_VAD")

View File

@@ -1,29 +0,0 @@
from esphome.components.mipi import DriverChip
import esphome.config_validation as cv
# Standalone display
# Product page: https://www.seeedstudio.com/reTerminal-D1001-p-6729.html
DriverChip(
"SEEED-RETERMINAL-D1001",
height=1280,
width=800,
hsync_back_porch=20,
hsync_pulse_width=20,
hsync_front_porch=40,
vsync_back_porch=12,
vsync_pulse_width=4,
vsync_front_porch=30,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
enable_pin=[{"xl9535": None, "number": 0}, {"xl9535": None, "number": 7}],
reset_pin={"xl9535": None, "number": 2},
initsequence=(
(0xE0, 0x00),
(0xE1, 0x93),
(0xE2, 0x65),
(0xE3, 0xF8),
(0x80, 0x01),
),
)

View File

@@ -1,51 +0,0 @@
from esphome.components.mipi import DriverChip
from esphome.config_validation import UNDEFINED
# fmt: off
sunton = DriverChip(
"ESP32-8048S070",
swap_xy=UNDEFINED,
initsequence=(),
width=800,
height=480,
pclk_frequency="12.5MHz",
de_pin=41,
hsync_pin=39,
vsync_pin=40,
pclk_pin=42,
hsync_pulse_width=30,
hsync_back_porch=16,
hsync_front_porch=210,
vsync_pulse_width=13,
vsync_back_porch=10,
vsync_front_porch=22,
data_pins={
"red": [14, 21, 47, 48, 45],
"green": [9, 46, 3, 8, 16, 1],
"blue": [15, 7, 6, 5, 4],
},
)
sunton.extend(
"ESP32-8048S050",
swap_xy=UNDEFINED,
initsequence=(),
width=800,
height=480,
pclk_frequency="16MHz",
de_pin=40,
hsync_pin=39,
vsync_pin=41,
pclk_pin=42,
hsync_back_porch=8,
hsync_front_porch=8,
hsync_pulse_width=4,
vsync_back_porch=8,
vsync_front_porch=8,
vsync_pulse_width=4,
data_pins={
"red": [45, 48, 47, 21, 14],
"green": [5, 6, 7, 15, 16, 4],
"blue": [8, 3, 46, 9, 1],
},
)

View File

@@ -1,6 +1,4 @@
from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER
from .ili import GC9A01A, ILI9341, ILI9342, ST7789V
from .ili import ILI9341, ILI9342, ST7789V
ILI9341.extend(
# ESP32-2432S028 CYD board with Micro USB, has ILI9341 controller
@@ -45,10 +43,3 @@ ILI9342.extend(
(0xE1, 0x00, 0x0B, 0x11, 0x05, 0x13, 0x09, 0x33, 0x67, 0x48, 0x07, 0x0E, 0x0B, 0x23, 0x33, 0x0F), # Negative Gamma Correction
)
)
GC9A01A.extend(
"ESP32-2424S012",
invert_colors=True,
cs_pin=10,
dc_pin={CONF_NUMBER: 2, CONF_IGNORE_STRAPPING_WARNING: True},
)

View File

@@ -555,7 +555,7 @@ ST7789V = DriverChip(
),
),
)
GC9A01A = DriverChip(
DriverChip(
"GC9A01A",
mirror_x=True,
width=240,

View File

@@ -5,29 +5,6 @@ namespace esphome::modbus::helpers {
static const char *const TAG = "modbus_helpers";
static size_t required_payload_size(SensorValueType sensor_value_type) {
switch (sensor_value_type) {
case SensorValueType::U_WORD:
case SensorValueType::S_WORD:
return 2;
case SensorValueType::U_DWORD:
case SensorValueType::FP32:
case SensorValueType::U_DWORD_R:
case SensorValueType::FP32_R:
case SensorValueType::S_DWORD:
case SensorValueType::S_DWORD_R:
return 4;
case SensorValueType::U_QWORD:
case SensorValueType::S_QWORD:
case SensorValueType::U_QWORD_R:
case SensorValueType::S_QWORD_R:
return 8;
case SensorValueType::RAW:
default:
return 0;
}
}
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
switch (value_type) {
case SensorValueType::U_WORD:
@@ -70,70 +47,93 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
uint32_t bitmask) {
int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
// Validate offset against the buffer for all types, including RAW/unsupported, so
// a malformed or misconfigured frame still produces an error log.
if (static_cast<size_t>(offset) > data.size()) {
ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu", static_cast<unsigned int>(sensor_value_type),
static_cast<unsigned int>(offset), data.size());
return value;
}
const size_t required_size = required_payload_size(sensor_value_type);
if (required_size == 0) {
return value;
}
if (data.size() - offset < required_size) {
ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu required=%zu",
static_cast<unsigned int>(sensor_value_type), static_cast<unsigned int>(offset), data.size(),
required_size);
if (offset > data.size()) {
ESP_LOGE(TAG, "not enough data for value");
return value;
}
size_t size = data.size() - offset;
bool error = false;
switch (sensor_value_type) {
case SensorValueType::U_WORD:
value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ;
if (size >= 2) {
value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset),
bitmask); // default is 0xFFFF ;
} else {
error = true;
}
break;
case SensorValueType::U_DWORD:
case SensorValueType::FP32:
value = get_data<uint32_t>(data, offset);
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
if (size >= 4) {
value = get_data<uint32_t>(data, offset);
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
} else {
error = true;
}
break;
case SensorValueType::U_DWORD_R:
case SensorValueType::FP32_R:
value = get_data<uint32_t>(data, offset);
value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
if (size >= 4) {
value = get_data<uint32_t>(data, offset);
value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
} else {
error = true;
}
break;
case SensorValueType::S_WORD:
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset), bitmask); // default is 0xFFFF ;
if (size >= 2) {
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
bitmask); // default is 0xFFFF ;
} else {
error = true;
}
break;
case SensorValueType::S_DWORD:
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
if (size >= 4) {
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
} else {
error = true;
}
break;
case SensorValueType::S_DWORD_R: {
value = get_data<uint32_t>(data, offset);
// Currently the high word is at the low position
// the sign bit is therefore at low before the switch
uint32_t sign_bit = (value & 0x8000) << 16;
value = mask_and_shift_by_rightbit(
static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
if (size >= 4) {
value = get_data<uint32_t>(data, offset);
// Currently the high word is at the low position
// the sign bit is therefore at low before the switch
uint32_t sign_bit = (value & 0x8000) << 16;
value = mask_and_shift_by_rightbit(
static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
} else {
error = true;
}
} break;
case SensorValueType::U_QWORD:
case SensorValueType::S_QWORD:
// Ignore bitmask for QWORD
value = get_data<uint64_t>(data, offset);
if (size >= 8) {
value = get_data<uint64_t>(data, offset);
} else {
error = true;
}
break;
case SensorValueType::U_QWORD_R:
case SensorValueType::S_QWORD_R: {
// Ignore bitmask for QWORD
uint64_t tmp = get_data<uint64_t>(data, offset);
value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
if (size >= 8) {
uint64_t tmp = get_data<uint64_t>(data, offset);
value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
} else {
error = true;
}
} break;
case SensorValueType::RAW:
default:
break;
}
if (error)
ESP_LOGE(TAG, "not enough data for value");
return value;
}
} // namespace esphome::modbus::helpers

View File

@@ -8,11 +8,8 @@ from typing import Any
from esphome import git, yaml_util
from esphome.components.substitutions import (
ContextVars,
ErrList,
push_context,
raise_first_undefined,
resolve_include,
resolve_substitutions_block,
substitute,
)
from esphome.components.substitutions.jinja import has_jinja
@@ -42,11 +39,6 @@ DOMAIN = CONF_PACKAGES
# Guard against infinite include chains (e.g. A includes B includes A).
MAX_INCLUDE_DEPTH = 20
PackageCallback = Callable[
[dict | str | yaml_util.IncludeFile, ContextVars | None, yaml_util.DocumentPath],
dict,
]
def is_remote_package(package_config: dict) -> bool:
"""Returns True if the package_config is a remote package definition."""
@@ -286,9 +278,8 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict:
def _walk_package_dict(
packages: dict,
callback: PackageCallback,
callback: Callable[[dict, ContextVars | None], dict],
context: ContextVars | None,
path: yaml_util.DocumentPath,
) -> cv.Invalid | None:
"""Iterate a packages dict in reverse priority order, invoking callback on each entry.
@@ -297,9 +288,7 @@ def _walk_package_dict(
for package_name, package_config in reversed(packages.items()):
with cv.prepend_path(package_name):
try:
packages[package_name] = callback(
package_config, context, path + [package_name]
)
packages[package_name] = callback(package_config, context)
except cv.Invalid as err:
return err
return None
@@ -307,22 +296,20 @@ def _walk_package_dict(
def _walk_package_list(
packages: list,
callback: PackageCallback,
callback: Callable[[dict, ContextVars | None], dict],
context: ContextVars | None,
path: yaml_util.DocumentPath,
) -> None:
"""Iterate a packages list in reverse priority order, invoking callback on each entry."""
for idx in reversed(range(len(packages))):
with cv.prepend_path(idx):
packages[idx] = callback(packages[idx], context, path + [idx])
packages[idx] = callback(packages[idx], context)
def _walk_packages(
config: dict,
callback: PackageCallback,
callback: Callable[[dict, ContextVars | None], dict],
context: ContextVars | None = None,
validate_deprecated: bool = True,
path: yaml_util.DocumentPath | None = None,
) -> dict:
"""Walks the packages structure in priority order, invoking ``callback`` on each package definition found.
@@ -333,24 +320,19 @@ def _walk_packages(
if CONF_PACKAGES not in config:
return config
packages = config[CONF_PACKAGES]
packages_path = (path or []) + [CONF_PACKAGES]
with cv.prepend_path(CONF_PACKAGES):
if isinstance(packages, yaml_util.IncludeFile):
# If the packages key is an IncludeFile, resolve it first before processing.
packages = resolve_include(
packages, packages_path, context, strict_undefined=False
)
packages, _ = resolve_include(packages, [], context, strict_undefined=False)
if not isinstance(packages, (dict, list)):
raise cv.Invalid(
f"Packages must be a key to value mapping or list, got {type(packages)} instead"
)
if not isinstance(packages, dict):
_walk_package_list(packages, callback, context, packages_path)
elif (
result := _walk_package_dict(packages, callback, context, packages_path)
) is not None:
_walk_package_list(packages, callback, context)
elif (result := _walk_package_dict(packages, callback, context)) is not None:
if not validate_deprecated or any(
is_package_definition(v) for v in packages.values()
):
@@ -359,18 +341,14 @@ def _walk_packages(
# This block can be removed once the single-package
# deprecation period (2026.7.0) is over.
config[CONF_PACKAGES] = [packages]
return _walk_packages(
deprecate_single_package(config), callback, context, path=path
)
return _walk_packages(deprecate_single_package(config), callback, context)
config[CONF_PACKAGES] = packages
return config
def _substitute_package_definition(
package_config: dict | str,
context_vars: ContextVars | None,
path: yaml_util.DocumentPath | None = None,
package_config: dict | str, context_vars: ContextVars | None
) -> dict | str:
"""Substitute variables in a package definition string or remote package dict.
@@ -381,19 +359,12 @@ def _substitute_package_definition(
if isinstance(package_config, str) or (
isinstance(package_config, dict) and is_remote_package(package_config)
):
# Collect undefined-variable errors (rather than raising strict) so the
# path walked through a remote-package dict is preserved and the user
# sees which field (url / path / ref / ...) referenced the undefined
# variable.
errors: ErrList = []
package_config = substitute(
item=package_config,
path=path or [],
path=[],
parent_context=context_vars or ContextVars(),
strict_undefined=False,
errors=errors,
)
raise_first_undefined(errors, "package definition")
return package_config
@@ -451,7 +422,6 @@ class _PackageProcessor:
self,
package_config: dict | str | yaml_util.IncludeFile,
context_vars: ContextVars | None,
path: yaml_util.DocumentPath,
) -> dict:
"""Resolve a package definition to a concrete ``dict`` and fetch remote packages.
@@ -474,15 +444,15 @@ class _PackageProcessor:
"""
for _ in range(MAX_INCLUDE_DEPTH):
if isinstance(package_config, yaml_util.IncludeFile):
package_config = resolve_include(
package_config, _ = resolve_include(
package_config,
path,
[],
context_vars or ContextVars(),
strict_undefined=False,
)
package_config = _substitute_package_definition(
package_config, context_vars, path
package_config, context_vars
)
package_config = PACKAGE_SCHEMA(package_config)
if isinstance(package_config, dict):
@@ -503,16 +473,13 @@ class _PackageProcessor:
_update_substitutions_context(self.parent_context, subs)
def process_package(
self,
package_config: dict | str,
context_vars: ContextVars | None,
path: yaml_util.DocumentPath,
self, package_config: dict | str, context_vars: ContextVars | None
) -> dict:
"""Resolve a single package and recurse into any nested packages."""
from_remote = isinstance(package_config, dict) and is_remote_package(
package_config
)
package_config = self.resolve_package(package_config, context_vars, path)
package_config = self.resolve_package(package_config, context_vars)
self.collect_substitutions(package_config)
if CONF_PACKAGES not in package_config:
@@ -532,7 +499,6 @@ class _PackageProcessor:
self.process_package,
context_vars,
validate_deprecated=not from_remote,
path=path,
)
@@ -550,12 +516,7 @@ def do_packages_pass(
if CONF_PACKAGES not in config:
return config
with cv.prepend_path(CONF_SUBSTITUTIONS):
substitutions = UserDict(
resolve_substitutions_block(
config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions
)
)
substitutions = UserDict(config.pop(CONF_SUBSTITUTIONS, {}))
processor = _PackageProcessor(
substitutions, command_line_substitutions, skip_update
)
@@ -589,13 +550,11 @@ def merge_packages(config: dict) -> dict:
merge_list: list[dict] = []
def process_package_callback(
package_config: dict,
context: ContextVars | None,
path: yaml_util.DocumentPath | None = None,
package_config: dict, context: ContextVars | None
) -> dict:
"""This will be called for each package found in the config."""
merge_list.append(package_config)
return _walk_packages(package_config, process_package_callback, path=path)
return _walk_packages(package_config, process_package_callback)
_walk_packages(config, process_package_callback, validate_deprecated=False)
# Merge all packages into the main config:

View File

@@ -24,8 +24,6 @@ static const uint8_t QMC5883L_REGISTER_CONTROL_1 = 0x09;
static const uint8_t QMC5883L_REGISTER_CONTROL_2 = 0x0A;
static const uint8_t QMC5883L_REGISTER_PERIOD = 0x0B;
void IRAM_ATTR QMC5883LComponent::gpio_intr(QMC5883LComponent *arg) { arg->enable_loop_soon_any_context(); }
void QMC5883LComponent::setup() {
// Soft Reset
if (!this->write_byte(QMC5883L_REGISTER_CONTROL_2, 1 << 7)) {
@@ -37,12 +35,6 @@ void QMC5883LComponent::setup() {
if (this->drdy_pin_) {
this->drdy_pin_->setup();
if (this->drdy_pin_->is_internal()) {
static_cast<InternalGPIOPin *>(this->drdy_pin_)
->attach_interrupt(&QMC5883LComponent::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
this->drdy_use_isr_ = true;
this->stop_poller();
}
}
uint8_t control_1 = 0;
@@ -73,8 +65,8 @@ void QMC5883LComponent::setup() {
return;
}
if (!this->drdy_use_isr_ && this->get_update_interval() < App.get_loop_interval()) {
this->high_freq_.start();
if (this->get_update_interval() < App.get_loop_interval()) {
high_freq_.start();
}
}
@@ -92,32 +84,16 @@ void QMC5883LComponent::dump_config() {
LOG_SENSOR(" ", "Heading", this->heading_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_PIN(" DRDY Pin: ", this->drdy_pin_);
if (this->drdy_pin_ != nullptr) {
ESP_LOGCONFIG(TAG, " DRDY mode: %s",
this->drdy_use_isr_ ? LOG_STR_LITERAL("interrupt") : LOG_STR_LITERAL("polling"));
}
}
void QMC5883LComponent::update() {
// If DRDY is on an external expander we keep the polling path and early-return
// if data is not ready yet. Internal DRDY pins take the ISR path via loop().
i2c::ErrorCode err;
uint8_t status = false;
// If DRDY pin is configured and the data is not ready return.
if (this->drdy_pin_ && !this->drdy_pin_->digital_read()) {
return;
}
this->read_sensor_();
}
void QMC5883LComponent::loop() {
this->disable_loop();
if (!this->drdy_use_isr_ || !this->drdy_pin_->digital_read()) {
return;
}
this->read_sensor_();
}
void QMC5883LComponent::read_sensor_() {
i2c::ErrorCode err;
uint8_t status = false;
// Status byte gets cleared when data is read, so we have to read this first.
// If status and two axes are desired, it's possible to save one byte of traffic by enabling

View File

@@ -32,7 +32,6 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
void setup() override;
void dump_config() override;
void update() override;
void loop() override;
void set_drdy_pin(GPIOPin *pin) { drdy_pin_ = pin; }
void set_datarate(QMC5883LDatarate datarate) { datarate_ = datarate; }
@@ -45,9 +44,6 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
protected:
static void IRAM_ATTR gpio_intr(QMC5883LComponent *arg);
void read_sensor_();
QMC5883LDatarate datarate_{QMC5883L_DATARATE_10_HZ};
QMC5883LRange range_{QMC5883L_RANGE_200_UT};
QMC5883LOversampling oversampling_{QMC5883L_SAMPLING_512};
@@ -57,7 +53,6 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice {
sensor::Sensor *heading_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
GPIOPin *drdy_pin_{nullptr};
bool drdy_use_isr_{false};
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,

View File

@@ -294,59 +294,57 @@ void Rtttl::play(std::string rtttl) {
}
ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
size_t name_end_position = this->position_;
size_t control_end = this->rtttl_.find(':', name_end_position + 1);
if (control_end == std::string::npos) {
ESP_LOGE(TAG, "Missing second ':'");
// Get default duration
this->position_ = this->rtttl_.find("d=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'd='");
return;
}
this->position_ += 2;
num = this->get_integer_();
if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) {
this->default_note_denominator_ = num;
} else {
ESP_LOGE(TAG, "Invalid default duration: %d", num);
return;
}
// Get default duration
size_t pos = this->rtttl_.find("d=", name_end_position);
if (pos == std::string::npos || pos >= control_end) {
ESP_LOGW(TAG, "Missing 'd='; use default duration %d", this->default_note_denominator_);
} else {
this->position_ = pos + 2;
num = this->get_integer_();
if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) {
this->default_note_denominator_ = num;
} else {
ESP_LOGE(TAG, "Invalid default duration: %d", num);
return;
}
}
// Get default octave
pos = this->rtttl_.find("o=", name_end_position);
if (pos == std::string::npos || pos >= control_end) {
ESP_LOGW(TAG, "Missing 'o='; use default octave %d", this->default_octave_);
this->position_ = this->rtttl_.find("o=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'o=");
return;
}
this->position_ += 2;
num = this->get_integer_();
if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) {
this->default_octave_ = num;
} else {
this->position_ = pos + 2;
num = this->get_integer_();
if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) {
this->default_octave_ = num;
} else {
ESP_LOGE(TAG, "Invalid default octave: %d", num);
return;
}
ESP_LOGE(TAG, "Invalid default octave: %d", num);
return;
}
// Get BPM
pos = this->rtttl_.find("b=", name_end_position);
if (pos == std::string::npos || pos >= control_end) {
ESP_LOGW(TAG, "Missing 'b='; use default BPM %d", bpm);
this->position_ = this->rtttl_.find("b=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing b=");
return;
}
this->position_ += 2;
num = this->get_integer_();
if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow
bpm = num;
} else {
this->position_ = pos + 2;
num = this->get_integer_();
if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow
bpm = num;
} else {
ESP_LOGE(TAG, "Invalid BPM: %d", num);
return;
}
ESP_LOGE(TAG, "Invalid BPM: %d", num);
return;
}
this->position_ = control_end + 1;
this->position_ = this->rtttl_.find(':', this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing second ':'");
return;
}
this->position_++;
// BPM usually expresses the number of quarter notes per minute
this->wholenote_duration_ = 60 * 1000L * 4 / bpm; // This is the time for whole note (in milliseconds)

View File

@@ -127,9 +127,9 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
uint32_t pos = this->get_position_(x, y);
Color mapped_color = color;
this->map_chroma_key(mapped_color);
this->buffer_[pos + 0] = mapped_color.b;
this->buffer_[pos + 0] = mapped_color.r;
this->buffer_[pos + 1] = mapped_color.g;
this->buffer_[pos + 2] = mapped_color.r;
this->buffer_[pos + 2] = mapped_color.b;
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
this->buffer_[pos + 3] = color.w;
}

View File

@@ -137,12 +137,11 @@ bool RuntimeStatsCollector::compare_total_time(Component *a, Component *b) {
return a->runtime_stats_.total_time_us > b->runtime_stats_.total_time_us;
}
// Slow path for process_pending_stats — gate already checked by the inline
// wrapper in runtime_stats.h. Out-of-line keeps the log_stats_ machinery out
// of Application::loop().
void RuntimeStatsCollector::process_pending_stats_slow_(uint32_t current_time) {
this->log_stats_();
this->next_log_time_ = current_time + this->log_interval_;
void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) {
if ((int32_t) (current_time - this->next_log_time_) >= 0) {
this->log_stats_();
this->next_log_time_ = current_time + this->log_interval_;
}
}
} // namespace runtime_stats

View File

@@ -6,7 +6,6 @@
#include <cstdint>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -27,24 +26,14 @@ class RuntimeStatsCollector {
}
uint32_t get_log_interval() const { return this->log_interval_; }
// Process any pending stats printing. Called on every Application::loop()
// tick, so the common "not yet time to log" path must be cheap — inline
// the gate check and keep the actual logging work out-of-line.
void ESPHOME_ALWAYS_INLINE process_pending_stats(uint32_t current_time) {
if ((int32_t) (current_time - this->next_log_time_) >= 0) [[unlikely]] {
this->process_pending_stats_slow_(current_time);
}
}
// Process any pending stats printing (should be called after component loop)
void process_pending_stats(uint32_t current_time);
// Record the wall time of one main loop iteration excluding the yield/sleep.
// Called once per loop from Application::loop().
// active_us = total time between loop start and just before yield.
// before_us = time spent in Phase A (scheduler tick) excluding time
// already attributed to per-component stats.
// tail_us = time spent in after_component_phase_ + the trailing record/stats
// prefix. Only meaningful on component-phase ticks; reported
// as 0 on Phase A-only ticks (no component phase ran, so any
// overhead between Phase A and stats belongs to "residual").
// before_us = time spent in before_loop_tasks_ (scheduler + ISR enable_loop).
// tail_us = time spent in after_loop_tasks_ + the trailing record/stats prefix.
// Residual overhead at log time = active Σ(component) before tail,
// which captures per-iteration inter-component bookkeeping (set_current_component,
// WarnIfComponentBlockingGuard construction/destruction, feed_wdt_with_time calls,
@@ -66,7 +55,6 @@ class RuntimeStatsCollector {
}
protected:
void process_pending_stats_slow_(uint32_t current_time);
void log_stats_();
// Static comparators — member functions have friend access, lambdas do not
static bool compare_period_time(Component *a, Component *b);

View File

@@ -87,7 +87,10 @@ async def to_code(config):
config[CONF_REBOOT_TIMEOUT],
config[CONF_BOOT_IS_GOOD_AFTER],
)
cg.add(RawExpression(f"if ({condition}) return"))
# Wrap in IIFEUnsafeStatement so cpp_main_section emits this
# component's block flat rather than inside an IIFE lambda —
# the `return` must exit setup() itself, not just the lambda.
cg.add(cg.IIFEUnsafeStatement(RawExpression(f"if ({condition}) return")))
CORE.data[CONF_SAFE_MODE] = {}
CORE.data[CONF_SAFE_MODE][KEY_PAST_SAFE_MODE] = True

View File

@@ -118,7 +118,6 @@ from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
DEVICE_CLASSES = [
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
DEVICE_CLASS_APPARENT_POWER,
@@ -276,9 +275,6 @@ ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter)
ThrottleWithPriorityFilter = sensor_ns.class_(
"ThrottleWithPriorityFilter", ValueListFilter
)
ThrottleWithPriorityNanFilter = sensor_ns.class_(
"ThrottleWithPriorityNanFilter", Filter
)
TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component)
TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase)
TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase)
@@ -294,7 +290,6 @@ SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter)
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
RoundFilter = sensor_ns.class_("RoundFilter", Filter)
RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter)
RoundSignificantDigitsFilter = sensor_ns.class_("RoundSignificantDigitsFilter", Filter)
validate_unit_of_measurement = cv.All(
cv.string_strict,
@@ -661,18 +656,9 @@ THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
THROTTLE_WITH_PRIORITY_SCHEMA,
)
async def throttle_with_priority_filter_to_code(config, filter_id):
values = config[CONF_VALUE]
if not isinstance(values, list):
values = [values]
# Specialize the common "NaN-only" case (the schema default when the user
# omits `value:`) to avoid the TemplatableFn<float> array + NaN lambda the
# generic ValueListFilter path requires. Behavior is identical: NaN sensor
# readings always bypass the throttle.
if values and all(isinstance(v, float) and math.isnan(v) for v in values):
filter_id = filter_id.copy()
filter_id.type = ThrottleWithPriorityNanFilter
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT])
template_ = [await cg.templatable(x, [], cg.float_) for x in values]
if not isinstance(config[CONF_VALUE], list):
config[CONF_VALUE] = [config[CONF_VALUE]]
template_ = [await cg.templatable(x, [], cg.float_) for x in config[CONF_VALUE]]
return cg.new_Pvariable(
filter_id, cg.TemplateArguments(len(template_)), config[CONF_TIMEOUT], template_
)
@@ -902,18 +888,6 @@ async def round_multiple_filter_to_code(config, filter_id):
)
@FILTER_REGISTRY.register(
"round_to_significant_digits",
RoundSignificantDigitsFilter,
cv.int_range(min=1, max=6),
)
async def round_significant_digits_filter_to_code(config, filter_id):
return cg.new_Pvariable(
filter_id,
cg.TemplateArguments(config),
)
async def build_filters(config):
return await cg.build_registry_list(FILTER_REGISTRY, config)

View File

@@ -269,18 +269,6 @@ optional<float> throttle_with_priority_new_value(Sensor *parent, float value, co
return {};
}
// ThrottleWithPriorityNanFilter
ThrottleWithPriorityNanFilter::ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs)
: min_time_between_inputs_(min_time_between_inputs) {}
optional<float> ThrottleWithPriorityNanFilter::new_value(float value) {
const uint32_t now = App.get_loop_component_start_time();
if (this->last_input_ == 0 || now - this->last_input_ >= this->min_time_between_inputs_ || std::isnan(value)) {
this->last_input_ = now;
return value;
}
return {};
}
// DeltaFilter
DeltaFilter::DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1)
: min_a0_(min_a0), min_a1_(min_a1), max_a0_(max_a0), max_a1_(max_a1) {}

View File

@@ -399,19 +399,6 @@ template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N>
uint32_t min_time_between_inputs_;
};
/// Specialization of ThrottleWithPriorityFilter for the common "prioritize NaN"
/// case: skips the TemplatableFn<float> array + lambda and inlines the check.
class ThrottleWithPriorityNanFilter : public Filter {
public:
explicit ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs);
optional<float> new_value(float value) override;
protected:
uint32_t last_input_{0};
uint32_t min_time_between_inputs_;
};
// Base class for timeout filters - contains common loop logic
class TimeoutFilterBase : public Filter, public Component {
public:
@@ -604,19 +591,6 @@ class RoundMultipleFilter : public Filter {
float multiple_;
};
template<uint8_t Digits> class RoundSignificantDigitsFilter : public Filter {
public:
optional<float> new_value(float value) override {
if (std::isfinite(value)) {
if (value == 0.0f)
return 0.0f;
float factor = pow10_int(Digits - 1 - ilog10(value));
return roundf(value * factor) / factor;
}
return value;
}
};
class ToNTCResistanceFilter : public Filter {
public:
ToNTCResistanceFilter(double a, double b, double c) : a_(a), b_(b), c_(c) {}

View File

@@ -14,34 +14,38 @@ BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) {
if (!monitor_loop || this->fd_ < 0)
return;
#ifdef USE_LWIP_FAST_SELECT
this->cached_sock_ = hook_fd_for_fast_select(this->fd_);
// Cache lwip_sock pointer and register for monitoring (hooks callback internally)
this->cached_sock_ = esphome_lwip_get_sock(this->fd_);
this->loop_monitored_ = App.register_socket(this->cached_sock_);
#else
this->loop_monitored_ = App.register_socket_fd(this->fd_);
#endif
}
BSDSocketImpl::~BSDSocketImpl() { this->close(); }
BSDSocketImpl::~BSDSocketImpl() {
if (!this->closed_) {
this->close();
}
}
int BSDSocketImpl::close() {
if (this->fd_ < 0) {
// Already closed, or never opened.
return 0;
}
if (!this->closed_) {
// Unregister before closing to avoid dangling pointer in monitored set
#ifdef USE_LWIP_FAST_SELECT
// Null the cached lwip_sock pointer before closing. The underlying lwip slot can be
// recycled for a new connection as soon as ::close() returns, so anything that might
// dereference cached_sock_ post-close (e.g. setsockopt(TCP_NODELAY)) would otherwise
// touch an unrelated socket's pcb. No per-socket callback unhook is needed —
// all LwIP sockets share the same static event_callback.
this->cached_sock_ = nullptr;
if (this->loop_monitored_) {
App.unregister_socket(this->cached_sock_);
this->cached_sock_ = nullptr;
}
#else
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
#endif
int ret = ::close(this->fd_);
this->fd_ = -1; // Sentinel for "closed" — prevents double-close and makes use-after-close visible.
return ret;
int ret = ::close(this->fd_);
this->closed_ = true;
return ret;
}
return 0;
}
int BSDSocketImpl::setblocking(bool blocking) {

View File

@@ -119,21 +119,12 @@ class BSDSocketImpl {
int get_fd() const { return this->fd_; }
protected:
// fd_ < 0 means "not open" — used both pre-open (initial state) and post-close. This
// replaces a separate closed_ flag: close() sets fd_ = -1 after ::close(), and the
// destructor / double-close path just check fd_ < 0.
int fd_{-1};
#ifdef USE_LWIP_FAST_SELECT
// Cached lwip_sock pointer used for direct rcvevent reads in ready() on the
// fast-select path. Replaces loop_monitored_: null means this socket is not being
// monitored for read events — either monitoring was not requested, the fd was
// invalid, or esphome_lwip_get_sock() failed. Non-null means the netconn event
// callback was hooked and notifications are flowing. close() nulls this to prevent
// use-after-free via a recycled lwip slot.
struct lwip_sock *cached_sock_{nullptr};
#else
bool loop_monitored_{false};
struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready()
#endif
bool closed_{false};
bool loop_monitored_{false};
};
} // namespace esphome::socket

View File

@@ -14,34 +14,38 @@ LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) {
if (!monitor_loop || this->fd_ < 0)
return;
#ifdef USE_LWIP_FAST_SELECT
this->cached_sock_ = hook_fd_for_fast_select(this->fd_);
// Cache lwip_sock pointer and register for monitoring (hooks callback internally)
this->cached_sock_ = esphome_lwip_get_sock(this->fd_);
this->loop_monitored_ = App.register_socket(this->cached_sock_);
#else
this->loop_monitored_ = App.register_socket_fd(this->fd_);
#endif
}
LwIPSocketImpl::~LwIPSocketImpl() { this->close(); }
LwIPSocketImpl::~LwIPSocketImpl() {
if (!this->closed_) {
this->close();
}
}
int LwIPSocketImpl::close() {
if (this->fd_ < 0) {
// Already closed, or never opened.
return 0;
}
if (!this->closed_) {
// Unregister before closing to avoid dangling pointer in monitored set
#ifdef USE_LWIP_FAST_SELECT
// Null the cached lwip_sock pointer before closing. The underlying lwip slot can be
// recycled for a new connection as soon as lwip_close() returns, so anything that
// might dereference cached_sock_ post-close (e.g. setsockopt(TCP_NODELAY)) would
// otherwise touch an unrelated socket's pcb. No per-socket callback unhook is needed —
// all LwIP sockets share the same static event_callback.
this->cached_sock_ = nullptr;
if (this->loop_monitored_) {
App.unregister_socket(this->cached_sock_);
this->cached_sock_ = nullptr;
}
#else
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
#endif
int ret = lwip_close(this->fd_);
this->fd_ = -1; // Sentinel for "closed" — prevents double-close and makes use-after-close visible.
return ret;
int ret = lwip_close(this->fd_);
this->closed_ = true;
return ret;
}
return 0;
}
int LwIPSocketImpl::setblocking(bool blocking) {

View File

@@ -85,21 +85,12 @@ class LwIPSocketImpl {
int get_fd() const { return this->fd_; }
protected:
// fd_ < 0 means "not open" — used both pre-open (initial state) and post-close. This
// replaces a separate closed_ flag: close() sets fd_ = -1 after lwip_close(), and the
// destructor / double-close path just check fd_ < 0.
int fd_{-1};
#ifdef USE_LWIP_FAST_SELECT
// Cached lwip_sock pointer used for direct rcvevent reads in ready() on the
// fast-select path. Replaces loop_monitored_: null means this socket is not being
// monitored for read events — either monitoring was not requested, the fd was
// invalid, or esphome_lwip_get_sock() failed. Non-null means the netconn event
// callback was hooked and notifications are flowing. close() nulls this to prevent
// use-after-free via a recycled lwip slot.
struct lwip_sock *cached_sock_{nullptr};
#else
bool loop_monitored_{false};
struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready()
#endif
bool closed_{false};
bool loop_monitored_{false};
};
} // namespace esphome::socket

View File

@@ -42,23 +42,8 @@ using ListenSocket = LWIPRawListenImpl;
#ifdef USE_LWIP_FAST_SELECT
/// Shared ready() helper using cached lwip_sock pointer for direct rcvevent read.
/// cached_sock == nullptr means the socket is not monitored (monitor_loop was false, fd
/// was invalid, or esphome_lwip_get_sock() failed) — in that case return true so the
/// caller attempts the read and handles blocking itself.
inline bool socket_ready(struct lwip_sock *cached_sock) {
return cached_sock == nullptr || esphome_lwip_socket_has_data(cached_sock);
}
/// Resolve an fd to its lwip_sock and install the netconn event-callback hook so the
/// main loop is woken by FreeRTOS task notifications when data arrives. Shared between
/// BSD and LwIP socket impls on the fast-select path. Returns the cached lwip_sock
/// pointer (or nullptr if the fd does not map to a valid lwip_sock).
inline struct lwip_sock *hook_fd_for_fast_select(int fd) {
struct lwip_sock *sock = esphome_lwip_get_sock(fd);
if (sock != nullptr) {
esphome_lwip_hook_socket(sock);
}
return sock;
inline bool socket_ready(struct lwip_sock *cached_sock, bool loop_monitored) {
return !loop_monitored || (cached_sock != nullptr && esphome_lwip_socket_has_data(cached_sock));
}
#elif defined(USE_HOST)
/// Shared ready() helper for fd-based socket implementations.
@@ -84,7 +69,7 @@ bool socket_ready_fd(int fd, bool loop_monitored);
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
inline bool Socket::ready() const {
#ifdef USE_LWIP_FAST_SELECT
return socket_ready(this->cached_sock_);
return socket_ready(this->cached_sock_, this->loop_monitored_);
#else
return socket_ready_fd(this->fd_, this->loop_monitored_);
#endif

View File

@@ -11,11 +11,9 @@ from esphome.types import ConfigType
from esphome.util import OrderedDict
from esphome.yaml_util import (
ConfigContext,
DocumentPath,
ESPHomeDataBase,
ESPLiteralValue,
IncludeFile,
format_path,
make_data_base,
)
@@ -25,42 +23,13 @@ CODEOWNERS = ["@esphome/core"]
_LOGGER = logging.getLogger(__name__)
ContextVars = ChainMap[str, Any]
ErrList = list[tuple[UndefinedError, DocumentPath, Any]]
SubstitutionPath = list[int | str]
ErrList = list[tuple[UndefinedError, SubstitutionPath, Any]]
# Module-level instance is safe: context_vars is passed per-call, and context_trace
# is stack-saved/restored within expand(). Not thread-safe — only use from one thread.
jinja = Jinja()
def raise_first_undefined(
errors: ErrList,
context_label: str,
) -> None:
"""If *errors* is non-empty, raise ``cv.Invalid`` for the first undefined variable.
The raised error names the missing variable and its location in the include
stack. Only the first error is surfaced; the user will re-run after fixing it
and any remaining undefined variables will be reported then.
``context_label`` is the noun describing where the undefined variable
appeared (e.g. ``"package definition"``).
"""
if not errors:
return
err, err_path, err_value = errors[0]
if len(errors) > 1:
# Log any further undefined variables so debug-level output covers
# the full set, even though only the first is surfaced to the user.
extras = ", ".join(
f"{e.message} at '{'->'.join(str(p) for p in p_path)}'"
for e, p_path, _ in errors[1:]
)
_LOGGER.debug("Additional undefined variables in %s: %s", context_label, extras)
raise cv.Invalid(
f"Undefined variable in {context_label}: {err.message}\n{format_path(err_path, err_value)}"
)
def validate_substitution_key(value: Any) -> str:
"""Validate and normalize a substitution key, stripping a leading ``$`` if present."""
value = cv.string(value)
@@ -126,7 +95,7 @@ def _resolve_var(name: str, context_vars: ContextVars) -> Any:
def _handle_undefined(
err: UndefinedError,
path: DocumentPath,
path: SubstitutionPath,
value: Any,
strict_undefined: bool,
errors: ErrList | None,
@@ -144,7 +113,7 @@ def _handle_undefined(
def _expand_substitutions(
value: str,
path: DocumentPath,
path: SubstitutionPath,
context_vars: ContextVars,
strict_undefined: bool,
errors: ErrList | None,
@@ -217,7 +186,7 @@ def _expand_substitutions(
f"\nEvaluation stack: (most recent evaluation last)"
f"\n{err.stack_trace_str()}"
f"\nRelevant context:\n{err.context_trace_str()}"
f"\n{format_path(path, orig_value)}",
f"\nSee {'->'.join(str(x) for x in path)}",
path,
) from err
else:
@@ -326,13 +295,15 @@ def push_context(
def resolve_include(
include: IncludeFile,
path: DocumentPath,
path: list[int | str],
context_vars: ContextVars,
strict_undefined: bool = True,
errors: ErrList | None = None,
) -> Any:
) -> tuple[Any, str]:
"""Resolve an include, substituting the filename if needed.
Returns the loaded content and the resolved filename.
Note: no path-traversal validation is performed on the resolved filename.
A substitution that resolves to an absolute path will bypass the parent
directory (Path.__truediv__ ignores the left operand for absolute paths).
@@ -340,44 +311,44 @@ def resolve_include(
values (including command-line substitutions), so path restrictions are
an explicit non-goal here.
"""
original = include.file
original_str = str(original)
original = str(include.file)
filename = str(
_expand_substitutions(
original_str, path + ["file"], context_vars, strict_undefined, errors
original, path + ["file"], context_vars, strict_undefined, errors
)
)
substituted = filename != original_str
if substituted:
if filename != original:
include = IncludeFile(
include.parent_file, filename, include.vars, include.yaml_loader
)
try:
return include.load()
return include.load(), filename
except esphome.core.EsphomeError as err:
resolved = f" (expanded from '{original}')" if substituted else ""
raise cv.Invalid(
f"Error including file '{filename}'{resolved}: {err}"
f"\n{format_path(path, original)}",
f"Error including file '{filename}': {err}",
path + [f"<{filename}>"],
) from err
def _substitute_include(
include: IncludeFile,
path: DocumentPath,
path: list[int | str],
context_vars: ContextVars,
strict_undefined: bool,
errors: ErrList | None,
) -> Any:
"""Resolve an include and substitute its content."""
content = resolve_include(include, path, context_vars, strict_undefined, errors)
return substitute(content, path, context_vars, strict_undefined, errors)
content, filename = resolve_include(
include, path, context_vars, strict_undefined, errors
)
return substitute(
content, path + [f"<{filename}>"], context_vars, strict_undefined, errors
)
def substitute(
item: Any,
path: DocumentPath,
path: SubstitutionPath,
parent_context: ContextVars,
strict_undefined: bool,
errors: ErrList | None = None,
@@ -430,43 +401,19 @@ def _warn_unresolved_variables(errors: ErrList) -> None:
for err, path, expression in errors:
if "password" in path:
continue
location: str = "->".join(str(x) for x in path)
if isinstance(expression, ESPHomeDataBase) and expression.esp_range is not None:
location += f" in {str(expression.esp_range.start_mark)}"
_LOGGER.warning(
"The string '%s' looks like an expression,"
" but could not resolve all the variables: %s\n%s",
" but could not resolve all the variables: %s (see %s)",
expression,
err.message,
format_path(path, expression),
location,
)
def resolve_substitutions_block(
substitutions: Any,
command_line_substitutions: dict[str, Any] | None,
) -> dict[str, Any]:
"""Resolve a deferred ``substitutions: !include file.yaml`` and validate the shape.
The caller is responsible for wrapping the call in
``cv.prepend_path(CONF_SUBSTITUTIONS)`` for error reporting.
``command_line_substitutions`` seeds the filename context so
``substitutions: !include ${var}.yaml`` can reference CLI-provided vars.
"""
if isinstance(substitutions, IncludeFile):
# Single-shot resolution — matches ``_walk_packages`` for the
# ``packages: !include`` entry point. Chained includes (an include that
# itself loads another ``!include`` at the top level) are not supported.
substitutions = resolve_include(
substitutions,
[],
ContextVars(command_line_substitutions or {}),
strict_undefined=False,
)
if not isinstance(substitutions, dict):
raise cv.Invalid(
f"Substitutions must be a key to value mapping, got {type(substitutions)}"
)
return substitutions
def do_substitution_pass(
config: OrderedDict, command_line_substitutions: dict[str, Any] | None = None
) -> OrderedDict:
@@ -482,9 +429,10 @@ def do_substitution_pass(
# Use merge_dicts_ordered to preserve OrderedDict type for move_to_end()
substitutions = config.pop(CONF_SUBSTITUTIONS, {})
with cv.prepend_path(CONF_SUBSTITUTIONS):
substitutions = resolve_substitutions_block(
substitutions, command_line_substitutions
)
if not isinstance(substitutions, dict):
raise cv.Invalid(
f"Substitutions must be a key to value mapping, got {type(substitutions)}"
)
substitutions = merge_dicts_ordered(
substitutions, command_line_substitutions or {}
)

View File

@@ -3,7 +3,6 @@ import esphome.codegen as cg
from esphome.components import text
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INITIAL_VALUE,
CONF_LAMBDA,
CONF_MAX_LENGTH,
@@ -13,7 +12,6 @@ from esphome.const import (
CONF_RESTORE_VALUE,
CONF_SET_ACTION,
)
from esphome.core import ID
from .. import template_ns
@@ -86,15 +84,8 @@ async def to_code(config):
if initial_value_config := config.get(CONF_INITIAL_VALUE):
cg.add(var.set_initial_value(initial_value_config))
if config[CONF_RESTORE_VALUE]:
saver_id = ID(
f"{config[CONF_ID].id}_value_saver",
is_declaration=True,
type=TextSaverBase,
)
saver_type = TextSaverTemplate.template(
cg.TemplateArguments(config[CONF_MAX_LENGTH])
)
saver = cg.Pvariable(saver_id, saver_type.new())
args = cg.TemplateArguments(config[CONF_MAX_LENGTH])
saver = TextSaverTemplate.template(args).new()
cg.add(var.set_value_saver(saver))
if CONF_SET_ACTION in config:

View File

@@ -101,10 +101,8 @@ void ZWaveProxy::loop() {
this->status_clear_warning();
}
void ZWaveProxy::process_uart_slow_() {
// Caller (inline process_uart_) has already confirmed available() > 0, so use do/while to
// drain bytes — available() is still checked at the tail, but not redundantly on entry.
do {
void ZWaveProxy::process_uart_() {
while (this->available()) {
uint8_t byte;
if (!this->read_byte(&byte)) {
this->status_set_warning(LOG_STR("UART read failed"));
@@ -139,7 +137,7 @@ void ZWaveProxy::process_uart_slow_() {
this->api_connection_->send_message(this->outgoing_proto_msg_);
}
}
} while (this->available());
}
}
void ZWaveProxy::dump_config() {
@@ -416,7 +414,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) {
}
}
bool ZWaveProxy::response_handler_slow_() {
bool ZWaveProxy::response_handler_() {
switch (this->parsing_state_) {
case ZWAVE_PARSING_STATE_SEND_ACK:
this->last_response_ = ZWAVE_FRAME_TYPE_ACK;

View File

@@ -38,13 +38,6 @@ enum ZWaveParsingState : uint8_t {
ZWAVE_PARSING_STATE_READ_BL_MENU,
};
// response_handler_()'s inline fast-path relies on SEND_ACK/CAN/NAK being contiguous in this
// enum so a single range check (state - SEND_ACK < 3) is equivalent to three equality checks.
static_assert(ZWAVE_PARSING_STATE_SEND_CAN == ZWAVE_PARSING_STATE_SEND_ACK + 1,
"SEND_CAN must immediately follow SEND_ACK for response_handler_ fast-path");
static_assert(ZWAVE_PARSING_STATE_SEND_NAK == ZWAVE_PARSING_STATE_SEND_ACK + 2,
"SEND_NAK must immediately follow SEND_CAN for response_handler_ fast-path");
enum ZWaveProxyFeature : uint32_t {
FEATURE_ZWAVE_PROXY_ENABLED = 1 << 0,
};
@@ -79,31 +72,8 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
void send_simple_command_(uint8_t command_id);
bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer)
void parse_start_(uint8_t byte);
// Inline fast-path: most calls happen with parsing_state_ outside the SEND_* range, so skip the
// out-of-line call entirely in the hot path (e.g. every loop() tick) and only pay for the real
// work when a response is actually pending. ESPHOME_ALWAYS_INLINE is required because with -Os
// gcc otherwise clones the wrapper into a shared $isra$ outline and keeps the call8.
ESPHOME_ALWAYS_INLINE bool response_handler_() {
if (this->parsing_state_ < ZWAVE_PARSING_STATE_SEND_ACK || this->parsing_state_ > ZWAVE_PARSING_STATE_SEND_NAK) {
return false;
}
return this->response_handler_slow_();
}
bool response_handler_slow_();
// Inline fast-path: UART::available() is cheap (ring-buffer head/tail compare on most backends).
// On an idle loop tick we want to skip the call to process_uart_ entirely. When bytes are
// pending we fall into the slow path, which drains the UART with a do/while so available() is
// only checked once per byte — no redundant re-check on entry.
ESPHOME_ALWAYS_INLINE void process_uart_() {
if (!this->available()) {
return;
}
this->process_uart_slow_();
}
// Precondition: caller must guarantee available() > 0 before invoking (see inline
// process_uart_ above). The slow path uses do/while and would otherwise set a spurious UART
// warning on entry if called with no bytes pending.
void process_uart_slow_();
bool response_handler_();
void process_uart_(); // Process all available UART data
// Pre-allocated message - always ready to send
api::ZWaveProxyFrame outgoing_proto_msg_;

View File

@@ -1,5 +1,6 @@
from collections import defaultdict
from contextlib import contextmanager
from dataclasses import dataclass, field
import logging
import math
import os
@@ -531,6 +532,126 @@ class Library:
return self
# Cap on the number of statements in a single IIFE chunk when a
# component's to_code body is sub-split. Picks a frame-size sweet spot
# on esp32-s3 — large enough that most components fit in one chunk and
# small enough that heavy sensor platforms (many filter registrations)
# don't produce a chunk with a very large spill frame.
IIFE_MAX_STATEMENTS = 50
@dataclass
class _ComponentGroup:
"""A contiguous run of statements emitted by one component's to_code."""
lines: list[str] = field(default_factory=list)
# True when the group contains a statement that must affect setup()'s
# own control flow (e.g. safe_mode's `return`). Emit the group flat,
# bypassing IIFE wrapping entirely.
unsafe: bool = False
# True when the group contains a statement that may declare a
# function-local whose lifetime extends past the current statement
# (scope-brace RawStatement, direct RawExpression, typed
# AssignmentExpression). Wrap the group in a single IIFE without
# sub-splitting so the declaration and any later references stay
# in the same lambda.
no_split: bool = False
def _emits_bare_local(exp: "Statement") -> bool:
"""True if ``exp`` emits a scope brace or bare-raw construct that may
declare a function-local whose lifetime extends past the current
statement. Components that emit any such statement must not be
sub-split — later references within the same ``to_code`` would land
in a different IIFE and fail to compile.
The detection is intentionally safety-biased: false negatives cause
silent broken C++, false positives just keep a component in one
slightly larger IIFE. Any ``cg.add(RawExpression(...))`` disables
sub-splitting for its group regardless of whether the raw text
actually references a local, because the chunker can't introspect
arbitrary raw text."""
from esphome.cpp_generator import (
AssignmentExpression,
ExpressionStatement,
RawExpression,
RawStatement,
)
# Scope braces from cg.with_local_variable() or inline scope blocks
# (e.g. time's tz pattern). Content-aware so RawStatements emitted
# for "call(); // comment" (entity_helpers) don't false-positive.
if isinstance(exp, RawStatement) and str(exp).strip() in ("{", "}"):
return True
# cg.add(RawExpression(...)) — bare raw text, e.g.
# `time::ParsedTimezone tz{}` or `tz.field = ...`. CORE.add wraps
# a passed Expression in an ExpressionStatement; when the inner is
# a RawExpression the author is emitting uninterpreted text that
# may reference a local declared elsewhere in the same block. A
# RawExpression passed as a CallExpression argument does NOT land
# here (its ExpressionStatement's .expression is the CallExpression),
# so value-pass patterns like `var.set_program(RawExpression("&foo"))`
# continue to sub-split normally.
if isinstance(exp, ExpressionStatement) and isinstance(
exp.expression, RawExpression
):
return True
# cg.variable(id, rhs) — emits ``Type id = rhs;`` as a function-local.
return (
isinstance(exp, ExpressionStatement)
and isinstance(exp.expression, AssignmentExpression)
and exp.expression.type is not None
)
def _wrap_in_iifes(lines: list[str], max_statements: int | None) -> list[str]:
"""Wrap ``lines`` in ``[]() {...}();`` IIFEs of up to ``max_statements``
each, or in a single IIFE when ``max_statements`` is ``None``. Never
splits inside a brace-balanced block (e.g. the ``{`` / ``}`` pair from
``cg.with_local_variable()``), so an IIFE may exceed the cap when a
block straddles it. Comment-only chunks pass through verbatim.
No ``noinline`` attribute — GCC's inliner re-folds small chunks freely,
keeping flash small without regressing peak stack."""
out: list[str] = []
chunk: list[str] = []
depth: int = 0
# Once depth goes negative we stop trusting the brace count and
# keep everything remaining in one final IIFE. A later ``{`` could
# arithmetically bring depth back to 0, but by that point the brace
# tracking is already unreliable — re-enabling mid-stream splits
# could land between a declaration and its use.
poisoned: bool = False
def flush() -> None:
if not chunk:
return
if all(line.lstrip().startswith("//") for line in chunk):
out.extend(chunk)
else:
out.append("[]() {")
out.extend(chunk)
out.append("}();")
chunk.clear()
for line in lines:
chunk.append(line)
# Count { and } per line so inline control flow (e.g. `if (cond) {`)
# and balanced inline lambdas are tracked correctly.
depth += line.count("{") - line.count("}")
if depth < 0:
poisoned = True
if (
not poisoned
and max_statements is not None
and depth == 0
and len(chunk) >= max_statements
):
flush()
flush()
return out
# pylint: disable=too-many-public-methods
class EsphomeCore:
def __init__(self):
@@ -1002,15 +1123,64 @@ class EsphomeCore:
self.data[KEY_CONTROLLER_REGISTRY_COUNT] = controller_count + 1
@property
def cpp_main_section(self):
from esphome.cpp_generator import statement
def cpp_main_section(self) -> str:
from esphome.cpp_generator import (
ComponentMarker,
IIFEUnsafeStatement,
statement,
)
main_code = []
# Split main_statements at ComponentMarker sentinels and wrap each
# component's group in an IIFE, sub-splitting at 50 statements so
# a single heavy component (e.g. a sensor platform with many
# filter registrations) can't blow the peak chunk frame.
#
# Two escape hatches control whether a component's group is safe
# to sub-split:
#
# - IIFEUnsafeStatement (e.g. safe_mode's setup-scope `return`):
# the whole group must stay at setup() scope so the statement
# affects setup()'s control flow, not the lambda's. Emit flat.
#
# - Any statement that may declare a function-local: a bare
# ``{`` / ``}`` RawStatement (from ``cg.with_local_variable``,
# time's inline tz block, etc.), a direct ``RawExpression``
# passed to ``cg.add`` (raw bare-local or field-assignment
# emission like ``time::ParsedTimezone tz`` followed by
# ``tz.field = ...``), or a typed ``AssignmentExpression``
# (``cg.variable`` emitting ``Type id = rhs;``). Each signals
# "this group's body may contain bare names whose scope is the
# enclosing IIFE"; wrap the whole group in one IIFE with no
# sub-split so the declaration and any later references stay
# together.
prefix: list[str] = []
components: list[_ComponentGroup] = []
current: list[str] = prefix
group: _ComponentGroup | None = None
for exp in self.main_statements:
text = str(statement(exp))
text = text.rstrip()
main_code.append(text)
return "\n".join(main_code) + "\n\n"
if isinstance(exp, ComponentMarker):
group = _ComponentGroup()
components.append(group)
current = group.lines
continue
if group is not None:
if isinstance(exp, IIFEUnsafeStatement):
group.unsafe = True
if _emits_bare_local(exp):
group.no_split = True
current.append(str(statement(exp)).rstrip())
if not components:
return "\n".join(prefix) + "\n\n"
pieces: list[str] = list(prefix)
for g in components:
if g.unsafe:
pieces.extend(g.lines)
else:
cap = None if g.no_split else IIFE_MAX_STATEMENTS
pieces.extend(_wrap_in_iifes(g.lines, max_statements=cap))
return "\n".join(pieces) + "\n\n"
@property
def cpp_global_section(self):

View File

@@ -1,229 +0,0 @@
#include "esphome/core/alloc_helpers.h"
#include "esphome/core/helpers.h"
#include <algorithm>
#include <cctype>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <string>
namespace esphome {
// --- String helpers ---
std::string str_truncate(const std::string &str, size_t length) {
return str.length() > length ? str.substr(0, length) : str;
}
std::string str_until(const char *str, char ch) {
const char *pos = strchr(str, ch);
return pos == nullptr ? std::string(str) : std::string(str, pos - str);
}
std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); }
// wrapper around std::transform to run safely on functions from the ctype.h header
// see https://en.cppreference.com/w/cpp/string/byte/toupper#Notes
template<int (*fn)(int)> std::string str_ctype_transform(const std::string &str) {
std::string result;
result.resize(str.length());
std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return fn(ch); });
return result;
}
std::string str_lower_case(const std::string &str) { return str_ctype_transform<std::tolower>(str); }
std::string str_upper_case(const std::string &str) {
std::string result;
result.resize(str.length());
std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return std::toupper(ch); });
return result;
}
std::string str_snake_case(const std::string &str) {
std::string result = str;
for (char &c : result) {
c = to_snake_case_char(c);
}
return result;
}
std::string str_sanitize(const std::string &str) {
std::string result;
result.resize(str.size());
str_sanitize_to(&result[0], str.size() + 1, str.c_str());
return result;
}
std::string str_snprintf(const char *fmt, size_t len, ...) {
std::string str;
va_list args;
str.resize(len);
va_start(args, len);
size_t out_length = vsnprintf(&str[0], len + 1, fmt, args);
va_end(args);
if (out_length < len)
str.resize(out_length);
return str;
}
std::string str_sprintf(const char *fmt, ...) {
std::string str;
va_list args;
va_start(args, fmt);
size_t length = vsnprintf(nullptr, 0, fmt, args);
va_end(args);
str.resize(length);
va_start(args, fmt);
vsnprintf(&str[0], length + 1, fmt, args);
va_end(args);
return str;
}
// --- Value formatting helpers ---
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
char buf[VALUE_ACCURACY_MAX_LEN];
value_accuracy_to_buf(buf, value, accuracy_decimals);
return std::string(buf);
}
// --- Base64 helpers ---
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
// Encode 3 input bytes to 4 base64 characters, append 'count' to ret.
static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) {
char char_array_4[4];
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (int j = 0; j < count; j++)
ret += BASE64_CHARS[static_cast<uint8_t>(char_array_4[j])];
}
std::string base64_encode(const std::vector<uint8_t> &buf) { return base64_encode(buf.data(), buf.size()); }
std::string base64_encode(const uint8_t *buf, size_t buf_len) {
std::string ret;
int i = 0;
char char_array_3[3];
while (buf_len--) {
char_array_3[i++] = *(buf++);
if (i == 3) {
base64_encode_triple(char_array_3, 4, ret);
i = 0;
}
}
if (i) {
for (int j = i; j < 3; j++)
char_array_3[j] = '\0';
base64_encode_triple(char_array_3, i + 1, ret);
while ((i++ < 3))
ret += '=';
}
return ret;
}
std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
// Calculate maximum decoded size: every 4 base64 chars = 3 bytes
size_t max_len = ((encoded_string.size() + 3) / 4) * 3;
std::vector<uint8_t> ret(max_len);
size_t actual_len = base64_decode(encoded_string, ret.data(), max_len);
ret.resize(actual_len);
return ret;
}
// --- Hex/binary formatting helpers ---
std::string format_mac_address_pretty(const uint8_t *mac) {
char buf[18];
format_mac_addr_upper(mac, buf);
return std::string(buf);
}
std::string format_hex(const uint8_t *data, size_t length) {
std::string ret;
ret.resize(length * 2);
format_hex_to(&ret[0], length * 2 + 1, data, length);
return ret;
}
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
// Shared implementation for uint8_t and string hex pretty formatting
static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) {
if (data == nullptr || length == 0)
return "";
std::string ret;
size_t hex_len = separator ? (length * 3 - 1) : (length * 2);
ret.resize(hex_len);
format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator);
if (show_length && length > 4)
return ret + " (" + std::to_string(length) + ")";
return ret;
}
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
return format_hex_pretty_uint8(data, length, separator, show_length);
}
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) {
return format_hex_pretty(data.data(), data.size(), separator, show_length);
}
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) {
if (data == nullptr || length == 0)
return "";
std::string ret;
size_t hex_len = separator ? (length * 5 - 1) : (length * 4);
ret.resize(hex_len);
format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator);
if (show_length && length > 4)
return ret + " (" + std::to_string(length) + ")";
return ret;
}
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator, bool show_length) {
return format_hex_pretty(data.data(), data.size(), separator, show_length);
}
std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length);
}
std::string format_bin(const uint8_t *data, size_t length) {
std::string result;
result.resize(length * 8);
format_bin_to(&result[0], length * 8 + 1, data, length);
return result;
}
// --- MAC address helpers ---
std::string get_mac_address() {
uint8_t mac[6];
get_mac_address_raw(mac);
char buf[13];
format_mac_addr_lower_no_sep(mac, buf);
return std::string(buf);
}
std::string get_mac_address_pretty() {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(get_mac_address_pretty_into_buffer(buf));
}
} // namespace esphome

View File

@@ -1,128 +0,0 @@
#pragma once
/// @file alloc_helpers.h
/// @brief Heap-allocating helper functions.
///
/// These functions return std::string and allocate heap memory on every call.
/// On long-running embedded devices, repeated heap allocations fragment memory
/// over time, eventually causing crashes even with free memory available.
///
/// Prefer the stack-based alternatives documented on each function instead.
/// New code should avoid using these functions.
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <string>
#include <vector>
namespace esphome {
// --- String helpers (allocating) ---
/// Truncate a string to a specific length.
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
std::string str_truncate(const std::string &str, size_t length);
/// Extract the part of the string until either the first occurrence of the specified character, or the end
/// (requires str to be null-terminated).
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
std::string str_until(const char *str, char ch);
/// Extract the part of the string until either the first occurrence of the specified character, or the end.
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
std::string str_until(const std::string &str, char ch);
/// Convert the string to lower case.
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
std::string str_lower_case(const std::string &str);
/// Convert the string to upper case.
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
std::string str_upper_case(const std::string &str);
/// Convert the string to snake case (lowercase with underscores).
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
std::string str_snake_case(const std::string &str);
/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores.
/// @warning Allocates heap memory. Use str_sanitize_to() with a stack buffer instead.
std::string str_sanitize(const std::string &str);
/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator).
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...);
/// sprintf-like function returning std::string.
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...);
// --- Hex/binary formatting helpers (allocating) ---
/// Format the six-byte array \p mac into a MAC address string.
/// @warning Allocates heap memory. Use format_mac_addr_upper() with a stack buffer instead.
std::string format_mac_address_pretty(const uint8_t mac[6]);
/// Format the byte array \p data of length \p len in lowercased hex.
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
std::string format_hex(const uint8_t *data, size_t length);
/// Format the vector \p data in lowercased hex.
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
std::string format_hex(const std::vector<uint8_t> &data);
/// Format a byte array in pretty-printed, human-readable hex format.
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true);
/// Format a 16-bit word array in pretty-printed, human-readable hex format.
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true);
/// Format a byte vector in pretty-printed, human-readable hex format.
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator = '.', bool show_length = true);
/// Format a 16-bit word vector in pretty-printed, human-readable hex format.
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator = '.', bool show_length = true);
/// Format a string's bytes in pretty-printed, human-readable hex format.
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true);
/// Format the byte array \p data of length \p len in binary.
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
std::string format_bin(const uint8_t *data, size_t length);
// --- Value formatting helpers (allocating) ---
/// Format a float value with accuracy decimals to a string.
/// @deprecated Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.
__attribute__((deprecated("Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.")))
std::string
value_accuracy_to_string(float value, int8_t accuracy_decimals);
// --- Base64 helpers (allocating) ---
/// Encode a byte buffer to base64 string.
/// @warning Allocates heap memory.
std::string base64_encode(const uint8_t *buf, size_t buf_len);
/// Encode a byte vector to base64 string.
/// @warning Allocates heap memory.
std::string base64_encode(const std::vector<uint8_t> &buf);
/// Decode a base64 string to a byte vector.
/// @warning Allocates heap memory. Use base64_decode(data, len, buf, buf_len) with a pre-allocated buffer instead.
std::vector<uint8_t> base64_decode(const std::string &encoded_string);
// --- MAC address helpers (allocating) ---
/// Get the device MAC address as a string, in lowercase hex notation.
/// @warning Allocates heap memory. Use get_mac_address_into_buffer() instead.
std::string get_mac_address();
/// Get the device MAC address as a string, in colon-separated uppercase hex notation.
/// @warning Allocates heap memory. Use get_mac_address_pretty_into_buffer() instead.
std::string get_mac_address_pretty();
} // namespace esphome

View File

@@ -93,11 +93,8 @@ void Application::setup() {
do {
uint32_t now = millis();
// Service scheduler and process pending loop enables to handle GPIO
// interrupts during setup. During setup we always run the component
// phase (no loop_interval_ gate), so call both helpers unconditionally.
this->scheduler_tick_(now);
this->before_component_phase_();
// Process pending loop enables to handle GPIO interrupts during setup
this->before_loop_tasks_(now);
for (uint32_t j = 0; j <= i; j++) {
// Update loop_component_start_time_ right before calling each component
@@ -106,7 +103,7 @@ void Application::setup() {
this->feed_wdt();
}
this->after_component_phase_();
this->after_loop_tasks_();
yield();
} while (!component->can_proceed() && !component->is_failed());
}
@@ -214,16 +211,11 @@ void Application::process_dump_config_() {
void Application::feed_wdt() {
// Cold entry: callers without a millis() timestamp in hand. Fetches the
// time and takes the same rate-limit paths as feed_wdt_with_time().
// time and takes the same rate-limit path as feed_wdt_with_time().
uint32_t now = millis();
if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) {
this->feed_wdt_slow_(now);
}
#ifdef USE_STATUS_LED
if (now - this->last_status_led_service_ > STATUS_LED_DISPATCH_INTERVAL_MS) {
this->service_status_led_slow_(now);
}
#endif
}
void HOT Application::feed_wdt_slow_(uint32_t time) {
@@ -231,35 +223,26 @@ void HOT Application::feed_wdt_slow_(uint32_t time) {
// confirmed the WDT_FEED_INTERVAL_MS rate limit was exceeded.
arch_feed_wdt();
this->last_wdt_feed_ = time;
}
#ifdef USE_STATUS_LED
void HOT Application::service_status_led_slow_(uint32_t time) {
// Callers (feed_wdt(), feed_wdt_with_time()) have already confirmed the
// STATUS_LED_DISPATCH_INTERVAL_MS rate limit was exceeded. Rate-limited
// separately from arch_feed_wdt() so the LED blink pattern stays readable
// (status_led error blink period is 250 ms) while HAL watchdog pokes can
// still run at the much coarser WDT_FEED_INTERVAL_MS cadence.
this->last_status_led_service_ = time;
if (status_led::global_status_led == nullptr)
return;
auto *sl = status_led::global_status_led;
uint8_t sl_state = sl->get_component_state() & COMPONENT_STATE_MASK;
if (sl_state == COMPONENT_STATE_LOOP_DONE) {
// status_led only transitions to LOOP_DONE from inside its own loop() (after the
// first idle-path dispatch), so its pin is already initialized by pre_setup() and
// its setup() has already run. Re-dispatch only if an error or warning bit has been
// set since; otherwise skip entirely.
if ((this->app_state_ & STATUS_LED_MASK) == 0)
if (status_led::global_status_led != nullptr) {
auto *sl = status_led::global_status_led;
uint8_t sl_state = sl->get_component_state() & COMPONENT_STATE_MASK;
if (sl_state == COMPONENT_STATE_LOOP_DONE) {
// status_led only transitions to LOOP_DONE from inside its own loop() (after the
// first idle-path dispatch), so its pin is already initialized by pre_setup() and
// its setup() has already run. Re-dispatch only if an error or warning bit has been
// set since; otherwise skip entirely.
if ((this->app_state_ & STATUS_LED_MASK) == 0)
return;
sl->enable_loop();
} else if (sl_state != COMPONENT_STATE_LOOP) {
// CONSTRUCTION/SETUP/FAILED: not our job — App::setup() drives the lifecycle.
return;
sl->enable_loop();
} else if (sl_state != COMPONENT_STATE_LOOP) {
// CONSTRUCTION/SETUP/FAILED: not our job — App::setup() drives the lifecycle.
return;
}
sl->loop();
}
sl->loop();
}
#endif
}
bool Application::any_component_has_status_flag_(uint8_t flag) const {
// Walk all components (not just looping ones) so non-looping components'
@@ -512,7 +495,32 @@ void Application::enable_pending_loops_() {
}
}
#ifdef USE_HOST
#ifdef USE_LWIP_FAST_SELECT
bool Application::register_socket(struct lwip_sock *sock) {
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
if (sock == nullptr)
return false;
esphome_lwip_hook_socket(sock);
this->monitored_sockets_.push_back(sock);
return true;
}
void Application::unregister_socket(struct lwip_sock *sock) {
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
for (size_t i = 0; i < this->monitored_sockets_.size(); i++) {
if (this->monitored_sockets_[i] != sock)
continue;
// Swap with last element and pop - O(1) removal since order doesn't matter.
// No need to unhook the netconn callback — all LwIP sockets share the same
// static event_callback, and the socket will be closed by the caller.
if (i < this->monitored_sockets_.size() - 1)
this->monitored_sockets_[i] = this->monitored_sockets_.back();
this->monitored_sockets_.pop_back();
return;
}
}
#elif defined(USE_HOST)
bool Application::register_socket_fd(int fd) {
// WARNING: This function is NOT thread-safe and must only be called from the main loop
// It modifies socket_fds_ and related variables without locking
@@ -702,10 +710,7 @@ void Application::get_comment_string(std::span<char, ESPHOME_COMMENT_SIZE_MAX> b
uint32_t Application::get_config_hash() { return ESPHOME_CONFIG_HASH; }
uint32_t Application::get_config_version_hash() {
constexpr uint32_t HASH = fnv1a_hash_extend(ESPHOME_CONFIG_HASH, ESPHOME_VERSION);
return HASH;
}
uint32_t Application::get_config_version_hash() { return fnv1a_hash_extend(ESPHOME_CONFIG_HASH, ESPHOME_VERSION); }
time_t Application::get_build_time() { return ESPHOME_BUILD_TIME; }

View File

@@ -229,50 +229,23 @@ class Application {
void schedule_dump_config() { this->dump_config_at_ = 0; }
/// Minimum interval between real arch_feed_wdt() calls. Sized so the outer
/// feed in Application::loop() is effectively rate-limited across both the
/// normal ~62 Hz cadence and worst-case wake-storm scenarios (e.g. external
/// stacks like OpenThread posting frequent wake notifications). Component
/// loops and scheduler items still feed after every op, so any op exceeding
/// this threshold triggers a real feed naturally.
/// Safety margins vs. platform watchdog timeouts:
/// - ESP32 task WDT default (5 s): ~16x
/// - ESP8266 soft WDT (~1.6 s): ~5x <-- floor case; any future change
/// must keep comfortable margin here
/// - ESP8266 HW WDT (~6 s): ~20x
static constexpr uint32_t WDT_FEED_INTERVAL_MS = 300;
/// Minimum interval between real arch_feed_wdt() calls. Chosen to keep the
/// rate of HAL pokes low while still being small enough that any plausible
/// watchdog timeout (seconds) has orders of magnitude of safety margin.
static constexpr uint32_t WDT_FEED_INTERVAL_MS = 3;
/// Feed the task watchdog. Cold entry — callers without a millis()
/// timestamp in hand. Out of line to keep call sites tiny.
void feed_wdt();
#ifdef USE_STATUS_LED
/// Dispatch interval for the status LED update. Deliberately shorter than
/// WDT_FEED_INTERVAL_MS because the status LED error blink has a 250 ms
/// period (status_led.cpp:ERROR_PERIOD_MS) and a 150 ms on-window; the
/// dispatch cadence must be short enough to render that blink without
/// aliasing. Sampling every 100 ms yields an on/off observation inside
/// every error period with headroom for the 250 ms warning on-window.
static constexpr uint32_t STATUS_LED_DISPATCH_INTERVAL_MS = 100;
#endif
/// Feed the task watchdog, hot entry. Callers that already have a
/// millis() timestamp pay only a load + sub + branch on the common
/// (no-op) path. The actual arch feed lives in feed_wdt_slow_.
/// When USE_STATUS_LED is compiled in, also gates a separate (shorter)
/// interval for dispatching status_led so the LED blink pattern stays
/// readable even though arch_feed_wdt pokes are now rate-limited at
/// WDT_FEED_INTERVAL_MS. The two rate limits are independent so raising
/// WDT_FEED_INTERVAL_MS does not distort the LED cadence.
/// (no-op) path. The actual arch feed + status LED update live in
/// feed_wdt_slow_.
void ESPHOME_ALWAYS_INLINE feed_wdt_with_time(uint32_t time) {
if (static_cast<uint32_t>(time - this->last_wdt_feed_) > WDT_FEED_INTERVAL_MS) [[unlikely]] {
this->feed_wdt_slow_(time);
}
#ifdef USE_STATUS_LED
if (static_cast<uint32_t>(time - this->last_status_led_service_) > STATUS_LED_DISPATCH_INTERVAL_MS) [[unlikely]] {
this->service_status_led_slow_(time);
}
#endif
}
void reboot();
@@ -345,13 +318,16 @@ class Application {
Scheduler scheduler;
#ifdef USE_HOST
/// Register/unregister a socket file descriptor with the host select() fallback loop.
/// USE_LWIP_FAST_SELECT builds do not use this API — sockets hook the lwIP netconn
/// event_callback directly (see socket.h hook_fd_for_fast_select) and rely on FreeRTOS
/// task notifications for wake-up.
/// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error.
/// Register/unregister a socket to be monitored for read events.
/// WARNING: These functions are NOT thread-safe. They must only be called from the main loop.
#ifdef USE_LWIP_FAST_SELECT
/// Fast select path: hooks netconn callback and registers for monitoring.
/// @return true if registration was successful, false if sock is null
bool register_socket(struct lwip_sock *sock);
void unregister_socket(struct lwip_sock *sock);
#elif defined(USE_HOST)
/// Fallback select() path: monitors file descriptors.
/// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error.
/// @return true if registration was successful, false if fd exceeds limits
bool register_socket_fd(int fd);
void unregister_socket_fd(int fd);
@@ -426,30 +402,19 @@ class Application {
void enable_component_loop_(Component *component);
void enable_pending_loops_();
void activate_looping_component_(uint16_t index);
inline uint32_t ESPHOME_ALWAYS_INLINE scheduler_tick_(uint32_t now);
inline void ESPHOME_ALWAYS_INLINE before_component_phase_();
inline void ESPHOME_ALWAYS_INLINE after_component_phase_() { this->in_loop_ = false; }
inline void ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time);
inline void ESPHOME_ALWAYS_INLINE after_loop_tasks_() { this->in_loop_ = false; }
/// Process dump_config output one component per loop iteration.
/// Extracted from loop() to keep cold startup/reconnect logging out of the hot path.
/// Caller must ensure dump_config_at_ < components_.size().
void __attribute__((noinline)) process_dump_config_();
/// Slow path for feed_wdt(): actually calls arch_feed_wdt() and updates
/// last_wdt_feed_. Out of line so the inline wrapper stays tiny. Does NOT
/// touch status_led — that's gated separately via service_status_led_slow_
/// because the two rate limits have very different safe ranges (~ seconds
/// for WDT, < 250 ms for LED blink rendering).
/// Slow path for feed_wdt(): actually calls arch_feed_wdt(), updates
/// last_wdt_feed_, and re-dispatches the status LED. Out of line so the
/// inline wrapper stays tiny.
void feed_wdt_slow_(uint32_t time);
#ifdef USE_STATUS_LED
/// Slow path for the status_led dispatch rate limit. Runs the status_led
/// component's loop() based on its state (LOOP / LOOP_DONE with status
/// bits set), and updates last_status_led_service_. Out of line to keep
/// the feed_wdt_with_time hot path a couple of load+branch sequences.
void service_status_led_slow_(uint32_t time);
#endif
/// Perform a delay while also monitoring socket file descriptors for readiness
#ifdef USE_HOST
// select() fallback path is too complex to inline (host platform)
@@ -486,7 +451,9 @@ class Application {
// and active_end_ is incremented
// - This eliminates branch mispredictions from flag checking in the hot loop
FixedVector<Component *> looping_components_{};
#ifdef USE_HOST
#ifdef USE_LWIP_FAST_SELECT
std::vector<struct lwip_sock *> monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read
#elif defined(USE_HOST)
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
#endif
#ifdef USE_HOST
@@ -501,10 +468,6 @@ class Application {
uint32_t last_loop_{0};
uint32_t loop_component_start_time_{0};
uint32_t last_wdt_feed_{0}; // millis() of most recent arch_feed_wdt(); rate-limits feed_wdt() hot path
#ifdef USE_STATUS_LED
// millis() of most recent status_led dispatch; rate-limits independently of last_wdt_feed_
uint32_t last_status_led_service_{0};
#endif
#ifdef USE_HOST
int max_fd_{-1}; // Highest file descriptor number for select()
@@ -583,25 +546,19 @@ inline void Application::drain_wake_notifications_() {
}
#endif // USE_HOST
// Phase A: drain wake notifications and run the scheduler. Invoked on every
// Application::loop() tick regardless of whether a component phase runs, so
// scheduler items fire at their requested cadence even when the caller has
// raised loop_interval_ for power savings (see Application::loop()).
// Returns the timestamp of the last scheduler item that ran (or `now`
// unchanged if none ran), so the caller's WDT feed stays monotonic with the
// per-item feeds inside scheduler.call() without an extra millis().
inline uint32_t ESPHOME_ALWAYS_INLINE Application::scheduler_tick_(uint32_t now) {
inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) {
#ifdef USE_HOST
// Drain wake notifications first to clear socket for next wake
this->drain_wake_notifications_();
#endif
return this->scheduler.call(now);
}
// Phase B entry: only invoked when a component loop phase is about to run.
// Processes pending enable_loop requests from ISRs and marks in_loop_ so
// reentrant modifications during component.loop() are safe.
inline void ESPHOME_ALWAYS_INLINE Application::before_component_phase_() {
// Process scheduled tasks. Scheduler::call now feeds the watchdog itself
// after each scheduled item that actually runs, so we no longer need an
// unconditional feed here — when Scheduler::call has no work to do, the
// only elapsed time is a sleep wake + a few instructions, and when it does
// have work, it fed the wdt as it went.
this->scheduler.call(loop_start_time);
// Process any pending enable_loop requests from ISRs
// This must be done before marking in_loop_ = true to avoid race conditions
if (this->has_pending_enable_loop_requests_) {
@@ -632,77 +589,40 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
// so charging it again to "before" would double-count.
uint64_t loop_recorded_snap = ComponentRuntimeStats::global_recorded_us;
#endif
// Phase A: always service the scheduler. Decouples scheduler cadence from
// loop_interval_ so raised intervals (for power savings) don't drag scheduled
// items forward. A tick that only runs the scheduler is cheap.
// scheduler_tick_ returns the timestamp of the last scheduler item that ran
// (advanced by its per-item feeds) or `now` unchanged. We adopt it as `now`
// so the gate check and WDT feed both reflect actual elapsed time after
// scheduler dispatch, without an extra millis() call.
uint32_t now = this->scheduler_tick_(millis());
// Guarantee one WDT feed per tick even when the scheduler had nothing to
// dispatch and the component phase is gated out — covers configs with no
// looping components and no scheduler work (setup() has its own
// per-component feed_wdt calls, so only do this here, not in scheduler_tick_).
this->feed_wdt_with_time(now);
// Get the initial loop time at the start
uint32_t last_op_end_time = millis();
this->before_loop_tasks_(last_op_end_time);
#ifdef USE_RUNTIME_STATS
uint32_t loop_before_end_us = micros();
uint64_t loop_before_scheduled_us = ComponentRuntimeStats::global_recorded_us - loop_recorded_snap;
// Only meaningful when do_component_phase is true; initialized to 0 so the
// tail bucket receives 0 on Phase A-only ticks (no component tail happened,
// the gate-check / stats-prefix overhead belongs to "residual", not "tail").
uint32_t loop_tail_start_us = 0;
#endif
// Gate the component phase on loop_interval_, an active high-frequency
// request, or an explicit wake from a background producer. A scheduler-only
// wake (e.g. set_interval firing under a raised loop_interval_) leaves the
// component phase gated; an external producer that called wake_loop_*
// (MQTT RX, USB RX, BLE event, etc.) needs the component phase to actually
// run so its component's loop() can drain the queued work — that is the
// long-standing semantic of wake_loop_threadsafe(), and the wake_request
// flag preserves it. wake_request_take() exchange-clears the flag; wakes
// that arrive during Phase B re-set it and run Phase B again on the next
// iteration.
const bool high_frequency = HighFrequencyLoopRequester::is_high_frequency();
const uint32_t elapsed = now - this->last_loop_;
const bool woke = esphome::wake_request_take();
const bool do_component_phase = high_frequency || woke || (elapsed >= this->loop_interval_);
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
this->current_loop_index_++) {
Component *component = this->looping_components_[this->current_loop_index_];
if (do_component_phase) {
this->before_component_phase_();
// Update the cached time before each component runs
this->loop_component_start_time_ = last_op_end_time;
uint32_t last_op_end_time = now;
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
this->current_loop_index_++) {
Component *component = this->looping_components_[this->current_loop_index_];
// Update the cached time before each component runs
this->loop_component_start_time_ = last_op_end_time;
{
this->set_current_component(component);
WarnIfComponentBlockingGuard guard{component, last_op_end_time};
component->loop();
// Use the finish method to get the current time as the end time
last_op_end_time = guard.finish();
}
this->feed_wdt_with_time(last_op_end_time);
{
this->set_current_component(component);
WarnIfComponentBlockingGuard guard{component, last_op_end_time};
component->loop();
// Use the finish method to get the current time as the end time
last_op_end_time = guard.finish();
}
#ifdef USE_RUNTIME_STATS
loop_tail_start_us = micros();
#endif
this->last_loop_ = last_op_end_time;
now = last_op_end_time;
this->after_component_phase_();
this->feed_wdt_with_time(last_op_end_time);
}
#ifdef USE_RUNTIME_STATS
// Record per-tick timing on every loop, not just component-phase ticks.
// record_loop_active is a small accumulator; process_pending_stats is an
// inline gate check that early-outs unless now >= next_log_time_.
uint32_t loop_tail_start_us = micros();
#endif
this->after_loop_tasks_();
#ifdef USE_RUNTIME_STATS
// Process any pending runtime stats printing after all components have run
// This ensures stats printing doesn't affect component timing measurements
if (global_runtime_stats != nullptr) {
uint32_t loop_now_us = micros();
// Subtract scheduled-component time from the "before" bucket so it is
@@ -711,40 +631,25 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
uint32_t loop_before_overhead_us = loop_before_wall_us > loop_before_scheduled_us
? loop_before_wall_us - static_cast<uint32_t>(loop_before_scheduled_us)
: 0;
// tail_us is only defined when Phase B ran; 0 on Phase A-only ticks so the
// stats bucket keeps its "component-phase trailing overhead" meaning.
uint32_t loop_tail_us = do_component_phase ? (loop_now_us - loop_tail_start_us) : 0;
global_runtime_stats->record_loop_active(loop_now_us - loop_active_start_us, loop_before_overhead_us, loop_tail_us);
global_runtime_stats->process_pending_stats(now);
global_runtime_stats->record_loop_active(loop_now_us - loop_active_start_us, loop_before_overhead_us,
loop_now_us - loop_tail_start_us);
global_runtime_stats->process_pending_stats(last_op_end_time);
}
#endif
// Compute sleep: bounded by time-until-next-component-phase and the
// scheduler's next deadline. When a scheduler timer fires it re-enters
// loop(), Phase A services it, and the component phase stays gated by
// loop_interval_. When a background producer calls wake_loop_threadsafe()
// it sets the wake_request flag and wakes select() / the task notification;
// the gate above sees the flag and runs Phase B too so the producer's
// component can drain its queued work without waiting up to loop_interval_.
//
// Re-read HighFrequencyLoopRequester::is_high_frequency() here instead of
// reusing the cached `high_frequency` captured above: a component calling
// HighFrequencyLoopRequester::start() from within its loop() would
// otherwise sit under the stale value and sleep for up to loop_interval_
// before the request took effect. That was fine pre-decoupling (the old
// main loop also called the function fresh at the sleep point) but now
// matters much more — loop_interval_ is a power-saving knob documented
// to accept multi-second values, so the stale path could add seconds of
// latency on an HF request. The call is a trivial atomic read.
// Use the last component's end time instead of calling millis() again
uint32_t delay_time = 0;
if (!HighFrequencyLoopRequester::is_high_frequency()) {
const uint32_t elapsed_since_phase = now - this->last_loop_;
const uint32_t until_phase =
(elapsed_since_phase >= this->loop_interval_) ? 0 : (this->loop_interval_ - elapsed_since_phase);
const uint32_t until_sched = this->scheduler.next_schedule_in(now).value_or(until_phase);
delay_time = std::min(until_phase, until_sched);
auto elapsed = last_op_end_time - this->last_loop_;
if (elapsed < this->loop_interval_ && !HighFrequencyLoopRequester::is_high_frequency()) {
delay_time = this->loop_interval_ - elapsed;
uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time);
// next_schedule is max 0.5*delay_time
// otherwise interval=0 schedules result in constant looping with almost no sleep
next_schedule = std::max(next_schedule, delay_time / 2);
delay_time = std::min(next_schedule, delay_time);
}
this->yield_with_select_(delay_time);
this->last_loop_ = last_op_end_time;
if (this->dump_config_at_ < this->components_.size()) {
this->process_dump_config_();
@@ -755,16 +660,26 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() {
#ifndef USE_HOST
inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) {
#ifdef USE_LWIP_FAST_SELECT
// Fast path (ESP32/LibreTiny): FreeRTOS task notifications posted by the lwip
// event_callback wrapper (see lwip_fast_select.c) are the single source of truth for
// socket wake-ups. Every NETCONN_EVT_RCVPLUS posts an xTaskNotifyGive, so any notification
// that lands between wakes keeps the counter non-zero (next ulTaskNotifyTake returns
// immediately) or wakes a blocked Take directly. Additional wake sources:
// wake_loop_threadsafe() from background tasks, and the delay_ms timeout.
// Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers.
// Safe because this runs on the main loop which owns socket lifetime (create, read, close).
if (delay_ms == 0) [[unlikely]] {
yield();
return;
}
// Check if any socket already has pending data before sleeping.
// If a socket still has unread data (rcvevent > 0) but the task notification was already
// consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency.
// This scan preserves select() semantics: return immediately when any fd is ready.
for (struct lwip_sock *sock : this->monitored_sockets_) {
if (esphome_lwip_socket_has_data(sock)) {
yield();
return;
}
}
// Sleep with instant wake via FreeRTOS task notification.
// Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout.
#endif
esphome::internal::wakeable_delay(delay_ms);
}

View File

@@ -601,7 +601,7 @@ class Component {
*/
class PollingComponent : public Component {
public:
PollingComponent() : PollingComponent(SCHEDULER_DONT_RUN) {}
PollingComponent() : PollingComponent(0) {}
/** Initialize this polling component with the given update interval in ms.
*

View File

@@ -48,7 +48,6 @@
#define USE_ENTITY_DEVICE_CLASS
#define USE_ENTITY_ICON
#define USE_ENTITY_UNIT_OF_MEASUREMENT
#define USE_ESP32_BLE_PSRAM
#define USE_ESP32_CAMERA_JPEG_CONVERSION
#define USE_ESP32_HOSTED
#define USE_ESP32_IMPROV_STATE_CALLBACK
@@ -178,7 +177,6 @@
#define USE_API_USER_DEFINED_ACTION_RESPONSES
#define USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#define API_MAX_SEND_QUEUE 8
#define MAX_API_CONNECTIONS 6
#define USE_MD5
#define USE_SHA256
#define USE_MQTT

View File

@@ -221,7 +221,31 @@ bool str_endswith_ignore_case(const char *str, size_t str_len, const char *suffi
return strncasecmp(str + str_len - suffix_len, suffix, suffix_len) == 0;
}
// str_truncate, str_until, str_lower_case, str_upper_case, str_snake_case moved to alloc_helpers.cpp
std::string str_truncate(const std::string &str, size_t length) {
return str.length() > length ? str.substr(0, length) : str;
}
std::string str_until(const char *str, char ch) {
const char *pos = strchr(str, ch);
return pos == nullptr ? std::string(str) : std::string(str, pos - str);
}
std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); }
// wrapper around std::transform to run safely on functions from the ctype.h header
// see https://en.cppreference.com/w/cpp/string/byte/toupper#Notes
template<int (*fn)(int)> std::string str_ctype_transform(const std::string &str) {
std::string result;
result.resize(str.length());
std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return fn(ch); });
return result;
}
std::string str_lower_case(const std::string &str) { return str_ctype_transform<std::tolower>(str); }
std::string str_upper_case(const std::string &str) { return str_ctype_transform<std::toupper>(str); }
std::string str_snake_case(const std::string &str) {
std::string result = str;
for (char &c : result) {
c = to_snake_case_char(c);
}
return result;
}
char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) {
if (buffer_size == 0) {
return buffer;
@@ -234,7 +258,41 @@ char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) {
return buffer;
}
// str_sanitize, str_snprintf, str_sprintf moved to alloc_helpers.cpp
std::string str_sanitize(const std::string &str) {
std::string result;
result.resize(str.size());
str_sanitize_to(&result[0], str.size() + 1, str.c_str());
return result;
}
std::string str_snprintf(const char *fmt, size_t len, ...) {
std::string str;
va_list args;
str.resize(len);
va_start(args, len);
size_t out_length = vsnprintf(&str[0], len + 1, fmt, args);
va_end(args);
if (out_length < len)
str.resize(out_length);
return str;
}
std::string str_sprintf(const char *fmt, ...) {
std::string str;
va_list args;
va_start(args, fmt);
size_t length = vsnprintf(nullptr, 0, fmt, args);
va_end(args);
str.resize(length);
va_start(args, fmt);
vsnprintf(&str[0], length + 1, fmt, args);
va_end(args);
return str;
}
// Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term)
static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128;
@@ -283,7 +341,11 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
return chars;
}
// format_mac_address_pretty moved to alloc_helpers.cpp
std::string format_mac_address_pretty(const uint8_t *mac) {
char buf[18];
format_mac_addr_upper(mac, buf);
return std::string(buf);
}
// Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase.
// When separator is set, it is written unconditionally after each byte and the last
@@ -336,7 +398,13 @@ char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_
return format_hex_internal(buffer, buffer_size, data, length, 0, 'a');
}
// format_hex (std::string returning overloads) moved to alloc_helpers.cpp
std::string format_hex(const uint8_t *data, size_t length) {
std::string ret;
ret.resize(length * 2);
format_hex_to(&ret[0], length * 2 + 1, data, length);
return ret;
}
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) {
return format_hex_internal(buffer, buffer_size, data, length, separator, 'A');
@@ -373,7 +441,43 @@ char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint16_t *dat
return buffer;
}
// format_hex_pretty (all std::string returning overloads) moved to alloc_helpers.cpp
// Shared implementation for uint8_t and string hex formatting
static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) {
if (data == nullptr || length == 0)
return "";
std::string ret;
size_t hex_len = separator ? (length * 3 - 1) : (length * 2);
ret.resize(hex_len);
format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator);
if (show_length && length > 4)
return ret + " (" + std::to_string(length) + ")";
return ret;
}
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
return format_hex_pretty_uint8(data, length, separator, show_length);
}
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) {
return format_hex_pretty(data.data(), data.size(), separator, show_length);
}
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) {
if (data == nullptr || length == 0)
return "";
std::string ret;
size_t hex_len = separator ? (length * 5 - 1) : (length * 4);
ret.resize(hex_len);
format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator);
if (show_length && length > 4)
return ret + " (" + std::to_string(length) + ")";
return ret;
}
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator, bool show_length) {
return format_hex_pretty(data.data(), data.size(), separator, show_length);
}
std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length);
}
char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) {
if (buffer_size == 0) {
@@ -396,7 +500,12 @@ char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_
return buffer;
}
// format_bin moved to alloc_helpers.cpp
std::string format_bin(const uint8_t *data, size_t length) {
std::string result;
result.resize(length * 8);
format_bin_to(&result[0], length * 8 + 1, data, length);
return result;
}
ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
if (on == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("on")) == 0)
@@ -413,23 +522,6 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
return PARSE_NONE;
}
int8_t ilog10(float value) {
float abs_val = fabsf(value);
int8_t exp = 0;
if (abs_val >= 10.0f) {
while (abs_val >= 10.0f) {
abs_val /= 10.0f;
exp++;
}
} else if (abs_val < 1.0f) {
while (abs_val < 1.0f) {
abs_val *= 10.0f;
exp--;
}
}
return exp;
}
static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) {
if (accuracy_decimals < 0) {
float divisor;
@@ -445,7 +537,11 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de
}
}
// value_accuracy_to_string moved to alloc_helpers.cpp
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
char buf[VALUE_ACCURACY_MAX_LEN];
value_accuracy_to_buf(buf, value, accuracy_decimals);
return std::string(buf);
}
size_t value_accuracy_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> buf, float value, int8_t accuracy_decimals) {
normalize_accuracy_decimals(value, accuracy_decimals);
@@ -510,7 +606,45 @@ static inline uint8_t base64_find_char(char c) {
// Check if character is valid base64 or base64url
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/') || (c == '-') || (c == '_')); }
// base64_encode (both overloads) moved to alloc_helpers.cpp
std::string base64_encode(const std::vector<uint8_t> &buf) { return base64_encode(buf.data(), buf.size()); }
// Encode 3 input bytes to 4 base64 characters, append 'count' to ret.
static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) {
char char_array_4[4];
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (int j = 0; j < count; j++)
ret += BASE64_CHARS[static_cast<uint8_t>(char_array_4[j])];
}
std::string base64_encode(const uint8_t *buf, size_t buf_len) {
std::string ret;
int i = 0;
char char_array_3[3];
while (buf_len--) {
char_array_3[i++] = *(buf++);
if (i == 3) {
base64_encode_triple(char_array_3, 4, ret);
i = 0;
}
}
if (i) {
for (int j = i; j < 3; j++)
char_array_3[j] = '\0';
base64_encode_triple(char_array_3, i + 1, ret);
while ((i++ < 3))
ret += '=';
}
return ret;
}
size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) {
return base64_decode(reinterpret_cast<const uint8_t *>(encoded_string.data()), encoded_string.size(), buf, buf_len);
@@ -571,7 +705,14 @@ size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *b
return out;
}
// base64_decode (vector-returning overload) moved to alloc_helpers.cpp
std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
// Calculate maximum decoded size: every 4 base64 chars = 3 bytes
size_t max_len = ((encoded_string.size() + 3) / 4) * 3;
std::vector<uint8_t> ret(max_len);
size_t actual_len = base64_decode(encoded_string, ret.data(), max_len);
ret.resize(actual_len);
return ret;
}
/// Decode base64/base64url string directly into vector of little-endian int32 values
/// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted)
@@ -710,7 +851,18 @@ void HighFrequencyLoopRequester::stop() {
this->started_ = false;
}
// get_mac_address, get_mac_address_pretty moved to alloc_helpers.cpp
std::string get_mac_address() {
uint8_t mac[6];
get_mac_address_raw(mac);
char buf[13];
format_mac_addr_lower_no_sep(mac, buf);
return std::string(buf);
}
std::string get_mac_address_pretty() {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(get_mac_address_pretty_into_buffer(buf));
}
void get_mac_address_into_buffer(std::span<char, MAC_ADDRESS_BUFFER_SIZE> buf) {
uint8_t mac[6];

View File

@@ -21,12 +21,6 @@
#include "esphome/core/optional.h"
// Backward compatibility re-export of heap-allocating helpers.
// These functions have moved to alloc_helpers.h. External components should
// update their includes to use #include "esphome/core/alloc_helpers.h" directly.
// This re-export will be removed in 2026.11.0.
#include "esphome/core/alloc_helpers.h"
#ifdef USE_ESP8266
#include <Esp.h>
#include <pgmspace.h>
@@ -740,11 +734,6 @@ template<size_t STACK_SIZE, typename T = uint8_t> class SmallBufferWithHeapFallb
/// @name Mathematics
///@{
/// Compute floor(log10(fabs(value))) using iterative comparison.
/// Avoids pulling in __ieee754_logf/log10f (~1KB flash).
/// Only valid for finite, non-zero values.
int8_t ilog10(float value);
/// Compute 10^exp using iterative multiplication/division.
/// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents. // NOLINT
/// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals. // NOLINT
@@ -990,13 +979,27 @@ inline bool str_endswith_ignore_case(const std::string &str, const char *suffix)
return str_endswith_ignore_case(str.c_str(), str.size(), suffix, strlen(suffix));
}
// str_truncate moved to alloc_helpers.h - remove this include before 2026.11.0
/// Truncate a string to a specific length.
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
std::string str_truncate(const std::string &str, size_t length);
// str_until, str_lower_case, str_upper_case moved to alloc_helpers.h - remove this comment before 2026.11.0
/// Extract the part of the string until either the first occurrence of the specified character, or the end
/// (requires str to be null-terminated).
std::string str_until(const char *str, char ch);
/// Extract the part of the string until either the first occurrence of the specified character, or the end.
std::string str_until(const std::string &str, char ch);
/// Convert the string to lower case.
std::string str_lower_case(const std::string &str);
/// Convert the string to upper case.
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
std::string str_upper_case(const std::string &str);
/// Convert a single char to snake_case: lowercase and space to underscore.
constexpr char to_snake_case_char(char c) { return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; }
// str_snake_case moved to alloc_helpers.h - remove this comment before 2026.11.0
/// Convert the string to snake case (lowercase with underscores).
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
std::string str_snake_case(const std::string &str);
/// Sanitize a single char: keep alphanumerics, dashes, underscores; replace others with underscore.
constexpr char to_sanitized_char(char c) {
@@ -1019,7 +1022,9 @@ template<size_t N> inline char *str_sanitize_to(char (&buffer)[N], const char *s
return str_sanitize_to(buffer, N, str);
}
// str_sanitize moved to alloc_helpers.h - remove this comment before 2026.11.0
/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores.
/// @warning Allocates heap memory. Use str_sanitize_to() with a stack buffer instead.
std::string str_sanitize(const std::string &str);
/// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations.
/// This computes object_id hashes directly from names without creating an intermediate buffer.
@@ -1035,7 +1040,13 @@ inline uint32_t fnv1_hash_object_id(const char *str, size_t len) {
return hash;
}
// str_snprintf, str_sprintf moved to alloc_helpers.h - remove this comment before 2026.11.0
/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator).
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...);
/// sprintf-like function returning std::string.
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...);
#ifdef USE_ESP8266
// ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM)
@@ -1084,33 +1095,7 @@ __attribute__((format(printf, 4, 5))) inline size_t buf_append_printf(char *buf,
}
#endif
#ifdef USE_ESP8266
/// Safely append a PROGMEM string to buffer, returning new position (capped at size).
/// ESP8266 internal implementation — prefer the `buf_append_str` macro which wraps
/// literals with `PSTR()` automatically so they stay in flash instead of eating RAM.
/// @param buf Output buffer
/// @param size Total buffer size
/// @param pos Current position in buffer
/// @param str PROGMEM-resident string to append (must not be null)
/// @return New position after appending; returns `size` if `pos >= size`, otherwise
/// returns at most `size - 1` because one byte is reserved for the null terminator
inline size_t buf_append_str_p(char *buf, size_t size, size_t pos, PGM_P str) {
if (pos >= size) {
return size;
}
size_t remaining = size - pos - 1; // reserve space for null terminator
size_t len = strnlen_P(str, remaining);
memcpy_P(buf + pos, str, len);
pos += len;
buf[pos] = '\0';
return pos;
}
/// Safely append a string to buffer, returning new position (capped at size).
/// More efficient than buf_append_printf for plain string literals.
/// On ESP8266 the literal is wrapped with PSTR() so it stays in flash.
#define buf_append_str(buf, size, pos, str) buf_append_str_p(buf, size, pos, PSTR(str))
#else
/// Safely append a string to buffer, returning new position (capped at size).
/// Safely append a string to buffer without format parsing, returning new position (capped at size).
/// More efficient than buf_append_printf for plain string literals.
/// @param buf Output buffer
/// @param size Total buffer size
@@ -1122,16 +1107,15 @@ inline size_t buf_append_str(char *buf, size_t size, size_t pos, const char *str
return size;
}
size_t remaining = size - pos - 1; // reserve space for null terminator
size_t len = 0;
while (len < remaining && str[len] != '\0') {
len++;
size_t len = strlen(str);
if (len > remaining) {
len = remaining;
}
memcpy(buf + pos, str, len);
pos += len;
buf[pos] = '\0';
return pos;
}
#endif
/// Concatenate a name with a separator and suffix using an efficient stack-based approach.
/// This avoids multiple heap allocations during string construction.
@@ -1457,26 +1441,189 @@ inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) {
format_hex_to(output, MAC_ADDRESS_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE);
}
// format_mac_address_pretty, format_hex (all overloads) moved to alloc_helpers.h
// Remove this comment and the template overloads below before 2026.11.0
/// Format the six-byte array \p mac into a MAC address.
/// @warning Allocates heap memory. Use format_mac_addr_upper() with a stack buffer instead.
/// Causes heap fragmentation on long-running devices.
std::string format_mac_address_pretty(const uint8_t mac[6]);
/// Format the byte array \p data of length \p len in lowercased hex.
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
/// Causes heap fragmentation on long-running devices.
std::string format_hex(const uint8_t *data, size_t length);
/// Format the vector \p data in lowercased hex.
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
/// Causes heap fragmentation on long-running devices.
std::string format_hex(const std::vector<uint8_t> &data);
/// Format an unsigned integer in lowercased hex, starting with the most significant byte.
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
/// Causes heap fragmentation on long-running devices.
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex(T val) {
val = convert_big_endian(val);
return format_hex(reinterpret_cast<uint8_t *>(&val), sizeof(T));
}
/// Format the std::array \p data in lowercased hex.
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
/// Causes heap fragmentation on long-running devices.
template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &data) {
return format_hex(data.data(), data.size());
}
// format_hex_pretty (all overloads) moved to alloc_helpers.h
// Remove this comment and the template overload below before 2026.11.0
/** Format a byte array in pretty-printed, human-readable hex format.
*
* Converts binary data to a hexadecimal string representation with customizable formatting.
* Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator.
* Optionally includes the total byte count in parentheses at the end.
*
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
* Causes heap fragmentation on long-running devices.
*
* @param data Pointer to the byte array to format.
* @param length Number of bytes in the array.
* @param separator Character to use between hex bytes (default: '.').
* @param show_length Whether to append the byte count in parentheses (default: true).
* @return Formatted hex string, e.g., "A1.B2.C3.D4.E5 (5)" or "A1:B2:C3" depending on parameters.
*
* @note Returns empty string if data is nullptr or length is 0.
* @note The length will only be appended if show_length is true AND the length is greater than 4.
*
* Example:
* @code
* uint8_t data[] = {0xA1, 0xB2, 0xC3};
* format_hex_pretty(data, 3); // Returns "A1.B2.C3" (no length shown for <= 4 parts)
* uint8_t data2[] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5};
* format_hex_pretty(data2, 5); // Returns "A1.B2.C3.D4.E5 (5)"
* format_hex_pretty(data2, 5, ':'); // Returns "A1:B2:C3:D4:E5 (5)"
* format_hex_pretty(data2, 5, '.', false); // Returns "A1.B2.C3.D4.E5"
* @endcode
*/
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true);
/// Format an unsigned integer in pretty-printed, human-readable hex format.
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
/** Format a 16-bit word array in pretty-printed, human-readable hex format.
*
* Similar to the byte array version, but formats 16-bit words as 4-digit hex values.
*
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
* Causes heap fragmentation on long-running devices.
*
* @param data Pointer to the 16-bit word array to format.
* @param length Number of 16-bit words in the array.
* @param separator Character to use between hex words (default: '.').
* @param show_length Whether to append the word count in parentheses (default: true).
* @return Formatted hex string with 4-digit hex values per word.
*
* @note The length will only be appended if show_length is true AND the length is greater than 4.
*
* Example:
* @code
* uint16_t data[] = {0xA1B2, 0xC3D4};
* format_hex_pretty(data, 2); // Returns "A1B2.C3D4" (no length shown for <= 4 parts)
* uint16_t data2[] = {0xA1B2, 0xC3D4, 0xE5F6};
* format_hex_pretty(data2, 3); // Returns "A1B2.C3D4.E5F6 (3)"
* @endcode
*/
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true);
/** Format a byte vector in pretty-printed, human-readable hex format.
*
* Convenience overload for std::vector<uint8_t>. Formats each byte as a two-digit
* uppercase hex value with customizable separator.
*
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
* Causes heap fragmentation on long-running devices.
*
* @param data Vector of bytes to format.
* @param separator Character to use between hex bytes (default: '.').
* @param show_length Whether to append the byte count in parentheses (default: true).
* @return Formatted hex string representation of the vector contents.
*
* @note The length will only be appended if show_length is true AND the vector size is greater than 4.
*
* Example:
* @code
* std::vector<uint8_t> data = {0xDE, 0xAD, 0xBE, 0xEF};
* format_hex_pretty(data); // Returns "DE.AD.BE.EF" (no length shown for <= 4 parts)
* std::vector<uint8_t> data2 = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA};
* format_hex_pretty(data2); // Returns "DE.AD.BE.EF.CA (5)"
* format_hex_pretty(data2, '-'); // Returns "DE-AD-BE-EF-CA (5)"
* @endcode
*/
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator = '.', bool show_length = true);
/** Format a 16-bit word vector in pretty-printed, human-readable hex format.
*
* Convenience overload for std::vector<uint16_t>. Each 16-bit word is formatted
* as a 4-digit uppercase hex value in big-endian order.
*
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
* Causes heap fragmentation on long-running devices.
*
* @param data Vector of 16-bit words to format.
* @param separator Character to use between hex words (default: '.').
* @param show_length Whether to append the word count in parentheses (default: true).
* @return Formatted hex string representation of the vector contents.
*
* @note The length will only be appended if show_length is true AND the vector size is greater than 4.
*
* Example:
* @code
* std::vector<uint16_t> data = {0x1234, 0x5678};
* format_hex_pretty(data); // Returns "1234.5678" (no length shown for <= 4 parts)
* std::vector<uint16_t> data2 = {0x1234, 0x5678, 0x9ABC};
* format_hex_pretty(data2); // Returns "1234.5678.9ABC (3)"
* @endcode
*/
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator = '.', bool show_length = true);
/** Format a string's bytes in pretty-printed, human-readable hex format.
*
* Treats each character in the string as a byte and formats it in hex.
* Useful for debugging binary data stored in std::string containers.
*
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
* Causes heap fragmentation on long-running devices.
*
* @param data String whose bytes should be formatted as hex.
* @param separator Character to use between hex bytes (default: '.').
* @param show_length Whether to append the byte count in parentheses (default: true).
* @return Formatted hex string representation of the string's byte contents.
*
* @note The length will only be appended if show_length is true AND the string length is greater than 4.
*
* Example:
* @code
* std::string data = "ABC"; // ASCII: 0x41, 0x42, 0x43
* format_hex_pretty(data); // Returns "41.42.43" (no length shown for <= 4 parts)
* std::string data2 = "ABCDE";
* format_hex_pretty(data2); // Returns "41.42.43.44.45 (5)"
* @endcode
*/
std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true);
/** Format an unsigned integer in pretty-printed, human-readable hex format.
*
* Converts the integer to big-endian byte order and formats each byte as hex.
* The most significant byte appears first in the output string.
*
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
* Causes heap fragmentation on long-running devices.
*
* @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.).
* @param val The unsigned integer value to format.
* @param separator Character to use between hex bytes (default: '.').
* @param show_length Whether to append the byte count in parentheses (default: true).
* @return Formatted hex string with most significant byte first.
*
* @note The length will only be appended if show_length is true AND sizeof(T) is greater than 4.
*
* Example:
* @code
* uint32_t value = 0x12345678;
* format_hex_pretty(value); // Returns "12.34.56.78" (no length shown for <= 4 parts)
* uint64_t value2 = 0x123456789ABCDEF0;
* format_hex_pretty(value2); // Returns "12.34.56.78.9A.BC.DE.F0 (8)"
* format_hex_pretty(value2, ':'); // Returns "12:34:56:78:9A:BC:DE:F0 (8)"
* format_hex_pretty<uint16_t>(0x1234); // Returns "12.34"
* @endcode
*/
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0>
std::string format_hex_pretty(T val, char separator = '.', bool show_length = true) {
val = convert_big_endian(val);
@@ -1536,10 +1683,13 @@ inline char *format_bin_to(char (&buffer)[N], T val) {
return format_bin_to(buffer, reinterpret_cast<const uint8_t *>(&val), sizeof(T));
}
// format_bin moved to alloc_helpers.h - remove this comment and template overload before 2026.11.0
/// Format the byte array \p data of length \p len in binary.
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
/// Causes heap fragmentation on long-running devices.
std::string format_bin(const uint8_t *data, size_t length);
/// Format an unsigned integer in binary, starting with the most significant byte.
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
/// Causes heap fragmentation on long-running devices.
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_bin(T val) {
val = convert_big_endian(val);
return format_bin(reinterpret_cast<uint8_t *>(&val), sizeof(T));
@@ -1555,7 +1705,9 @@ enum ParseOnOffState : uint8_t {
/// Parse a string that contains either on, off or toggle.
ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr);
// value_accuracy_to_string moved to alloc_helpers.h - remove this comment before 2026.11.0
/// @deprecated Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.
ESPDEPRECATED("Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.", "2026.1.0")
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals);
/// Maximum buffer size for value_accuracy formatting (float ~15 chars + space + UOM ~40 chars + null)
static constexpr size_t VALUE_ACCURACY_MAX_LEN = 64;
@@ -1569,8 +1721,10 @@ size_t value_accuracy_with_uom_to_buf(std::span<char, VALUE_ACCURACY_MAX_LEN> bu
/// Derive accuracy in decimals from an increment step.
int8_t step_to_accuracy_decimals(float step);
// base64_encode (both overloads), base64_decode (vector overload) moved to alloc_helpers.h
// Remove this comment before 2026.11.0
std::string base64_encode(const uint8_t *buf, size_t buf_len);
std::string base64_encode(const std::vector<uint8_t> &buf);
std::vector<uint8_t> base64_decode(const std::string &encoded_string);
size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len);
size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len);
@@ -2006,7 +2160,15 @@ class HighFrequencyLoopRequester {
/// Get the device MAC address as raw bytes, written into the provided byte array (6 bytes).
void get_mac_address_raw(uint8_t *mac); // NOLINT(readability-non-const-parameter)
// get_mac_address, get_mac_address_pretty moved to alloc_helpers.h - remove this comment before 2026.11.0
/// Get the device MAC address as a string, in lowercase hex notation.
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
/// Use get_mac_address_into_buffer() instead.
std::string get_mac_address();
/// Get the device MAC address as a string, in colon-separated uppercase hex notation.
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
/// Use get_mac_address_pretty_into_buffer() instead.
std::string get_mac_address_pretty();
/// Get the device MAC address into the given buffer, in lowercase hex notation.
/// Assumes buffer length is MAC_ADDRESS_BUFFER_SIZE (12 digits for hexadecimal representation followed by null

View File

@@ -533,7 +533,7 @@ void HOT Scheduler::process_defer_queue_slow_path_(uint32_t &now) {
}
#endif /* not ESPHOME_THREAD_SINGLE */
uint32_t HOT Scheduler::call(uint32_t now) {
void HOT Scheduler::call(uint32_t now) {
#ifndef ESPHOME_THREAD_SINGLE
this->process_defer_queue_(now);
#endif /* not ESPHOME_THREAD_SINGLE */
@@ -703,9 +703,6 @@ uint32_t HOT Scheduler::call(uint32_t now) {
this->debug_verify_no_leak_();
}
#endif
// execute_item_() advances `now` as items fire; return it so the caller
// stays monotonic with last_wdt_feed_.
return now;
}
void HOT Scheduler::process_to_add_slow_path_() {
LockGuard guard{this->lock_};

View File

@@ -129,8 +129,7 @@ class Scheduler {
// Execute all scheduled items that are ready
// @param now Fresh timestamp from millis() - must not be stale/cached
// @return Timestamp of the last item that ran, or `now` unchanged if none ran.
uint32_t call(uint32_t now);
void call(uint32_t now);
// Move items from to_add_ into the main heap.
// IMPORTANT: This method should only be called from the main thread (loop task).

View File

@@ -1,14 +1,28 @@
#include "esphome/core/util.h"
#include "esphome/core/defines.h"
#include "esphome/core/application.h"
#include "esphome/core/version.h"
#include "esphome/core/log.h"
#ifdef USE_API
#include "esphome/components/api/api_server.h"
#endif
#ifdef USE_MQTT
#include "esphome/components/mqtt/mqtt_client.h"
#endif
namespace esphome {
bool api_is_connected() {
#ifdef USE_API
if (api::global_api_server != nullptr) {
return api::global_api_server->is_connected();
}
#endif
return false;
}
bool mqtt_is_connected() {
#ifdef USE_MQTT
if (mqtt::global_mqtt_client != nullptr) {

View File

@@ -1,28 +1,10 @@
#pragma once
#include <string>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_API
#include "esphome/components/api/api_server.h"
#endif
namespace esphome {
/// Return whether the node has at least one client connected to the native API.
///
/// Inline so that hot-path callers (e.g. component loop() ticks that check connectivity every
/// iteration) can skip the call8/return pair. With USE_API disabled this trivially returns false
/// and collapses at compile time.
#ifdef USE_API
ESPHOME_ALWAYS_INLINE inline bool api_is_connected() {
return api::global_api_server != nullptr && api::global_api_server->is_connected();
}
#else
ESPHOME_ALWAYS_INLINE inline bool api_is_connected() { return false; }
#endif
/// Return whether the node has at least one client connected to the native API
bool api_is_connected();
/// Return whether the node has an active connection to an MQTT broker
bool mqtt_is_connected();

View File

@@ -12,22 +12,9 @@
namespace esphome {
// === Wake-requested flag storage ===
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::atomic<uint8_t> g_wake_requested{0};
#else
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
volatile uint8_t g_wake_requested = 0;
#endif
// === ESP32 / LibreTiny — IRAM_ATTR entry points ===
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) {
// ISR-safe: set flag before notify so the wake is visible on the next gate
// check. wake_request_set() is just an aligned 8-bit store / atomic store
// and is safe from IRAM.
wake_request_set();
esphome_main_task_notify_from_isr(px_higher_priority_task_woken);
}
void IRAM_ATTR wake_loop_any_context() { wake_main_task_any_context(); }
@@ -85,9 +72,6 @@ void wakeable_delay(uint32_t ms) {
// === Host (UDP loopback socket) ===
#ifdef USE_HOST
void wake_loop_threadsafe() {
// Set flag before sending so the consumer's gate check on the next loop()
// entry observes the wake regardless of select() scheduling.
wake_request_set();
if (App.wake_socket_fd_ >= 0) {
const char dummy = 1;
::send(App.wake_socket_fd_, &dummy, 1, 0);

View File

@@ -7,10 +7,6 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
#include <atomic>
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
#include "esphome/core/main_task.h"
#endif
@@ -29,48 +25,12 @@ namespace esphome {
extern volatile bool g_main_loop_woke;
#endif
// === wake_request flag — signals Application::loop() that a producer queued
// work for some component's loop() to drain (MQTT RX, USB RX, BLE event, etc.)
// and the component phase should run this tick instead of being held off by
// the loop_interval_ gate. Set by every wake_loop_* entry point; consumed
// (via exchange-and-clear) at the gate in Application::loop(). ===
//
// std::atomic<uint8_t> rather than std::atomic<bool> because GCC on Xtensa
// generates an indirect function call for atomic<bool> ops instead of inlining
// them — same workaround applied in scheduler.h for the SchedulerItem::remove
// flag. On non-atomic platforms a volatile uint8_t suffices: 8-bit aligned
// loads/stores are atomic on every supported MCU, and the platform signal
// that follows wake_request_set() (FreeRTOS task-notify, esp_schedule, socket
// send) provides the cross-thread/cross-core memory barrier.
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern std::atomic<uint8_t> g_wake_requested;
__attribute__((always_inline)) inline void wake_request_set() { g_wake_requested.store(1, std::memory_order_release); }
__attribute__((always_inline)) inline bool wake_request_take() {
return g_wake_requested.exchange(0, std::memory_order_acquire) != 0;
}
#else
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern volatile uint8_t g_wake_requested;
__attribute__((always_inline)) inline void wake_request_set() { g_wake_requested = 1; }
__attribute__((always_inline)) inline bool wake_request_take() {
uint8_t v = g_wake_requested;
g_wake_requested = 0;
return v != 0;
}
#endif
// === ESP32 / LibreTiny (FreeRTOS) ===
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
/// Wake the main loop from any context (ISR or task).
/// always_inline so callers placed in IRAM keep the whole wake path in IRAM.
__attribute__((always_inline)) inline void wake_main_task_any_context() {
// Set the wake-requested flag BEFORE the task notification so the consumer
// (Application::loop() gate) is guaranteed to see it on its next gate check.
wake_request_set();
if (in_isr_context()) {
BaseType_t px_higher_priority_task_woken = pdFALSE;
esphome_main_task_notify_from_isr(&px_higher_priority_task_woken);
@@ -90,10 +50,7 @@ __attribute__((always_inline)) inline void wake_main_task_any_context() {
void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken);
void wake_loop_any_context();
inline void wake_loop_threadsafe() {
wake_request_set();
esphome_main_task_notify();
}
inline void wake_loop_threadsafe() { esphome_main_task_notify(); }
namespace internal {
inline void wakeable_delay(uint32_t ms) {
@@ -110,9 +67,6 @@ inline void wakeable_delay(uint32_t ms) {
/// Inline implementation — IRAM callers inline this directly.
inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() {
// Set the wake-requested flag BEFORE esp_schedule so the consumer is
// guaranteed to see it on its next gate check.
wake_request_set();
g_main_loop_woke = true;
esp_schedule();
}
@@ -144,9 +98,6 @@ inline void wakeable_delay(uint32_t ms) {
#elif defined(USE_RP2040)
inline void wake_loop_any_context() {
// Set the wake-requested flag BEFORE the SEV so the consumer is guaranteed
// to see it on its next gate check.
wake_request_set();
g_main_loop_woke = true;
__sev();
}

View File

@@ -434,6 +434,48 @@ class LineComment(Statement):
return "\n".join(parts)
class IIFEUnsafeStatement(Statement):
"""Statement that must not be placed inside an IIFE lambda when
``cpp_main_section`` chunks ``setup()``. Causes the containing
component's block to be emitted flat (no IIFE), so constructs that
rely on exiting ``setup()`` directly — e.g. safe_mode's
``if (should_enter_safe_mode(...)) return;`` — still work.
Accepts either a ``Statement`` or a bare ``Expression``; bare
expressions are wrapped so they terminate with a semicolon."""
__slots__ = ("inner",)
def __init__(self, inner: Expression | Statement) -> None:
self.inner = inner
def __str__(self) -> str:
return str(statement(self.inner))
class ComponentMarker(Statement):
"""Chunking-boundary sentinel. ``cpp_main_section`` wraps the
statements between two markers in an IIFE to shorten temporary
lifetimes and bound peak setup-time stack. Emits no C++ output.
Grouping is best-effort: ``flush_tasks`` can interleave coroutines
on ``await``, so a component's later statements may land in another
component's chunk. This is safe for the dominant codegen patterns
(placement-new into static storage, assignment to a file-scope
global); patterns that depend on function-local state within the
IIFE scope (cg.variable, with_local_variable, raw bare locals)
are kept together by the bare-local detection in cpp_main_section
so they aren't split across sibling lambdas."""
__slots__ = ("name",)
def __init__(self, name: str) -> None:
self.name = name
def __str__(self) -> str:
return f"// component-marker: {self.name}"
class ProgmemAssignmentExpression(AssignmentExpression):
__slots__ = ()
@@ -458,7 +500,13 @@ def progmem_array(id_, rhs) -> "MockObj":
rhs = safe_exp(rhs)
obj = MockObj(id_, ".")
assignment = ProgmemAssignmentExpression(id_.type, id_, rhs)
CORE.add(assignment)
# Emit at file scope, not inside setup(). setup() is split into
# per-component IIFE lambdas; a function-local static declared in one
# lambda is not visible to statements in sibling lambdas that
# reference the same shared table (e.g. two lights sharing a gamma
# lookup). File-scope static constexpr is semantically identical for
# read-only lookup tables.
CORE.add_global(assignment)
CORE.register_variable(id_, obj)
return obj
@@ -467,7 +515,7 @@ def static_const_array(id_, rhs) -> "MockObj":
rhs = safe_exp(rhs)
obj = MockObj(id_, ".")
assignment = StaticConstAssignmentExpression(id_.type, id_, rhs)
CORE.add(assignment)
CORE.add_global(assignment)
CORE.register_variable(id_, obj)
return obj
@@ -490,10 +538,15 @@ def literal(name: str) -> "MockObj":
def variable(
id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True
id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register: bool = True
) -> "MockObj":
"""Declare a new variable, not pointer type, in the code generation.
Emits a function-local declaration ``Type id = rhs;`` inside setup().
``cpp_main_section`` detects typed ``AssignmentExpression`` and
disables sub-chunking for the component's group, so later references
to the local within the same ``to_code`` stay visible.
:param id_: The ID used to declare the variable.
:param rhs: The expression to place on the right hand side of the assignment.
:param type_: Manually define a type for the variable, only use this when it's not possible
@@ -606,43 +659,33 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
if isinstance(rhs, MockObj) and rhs.is_new_expr:
# For 'new' allocations, use placement new into static storage
# to avoid heap fragmentation on embedded devices.
#
# Storage must be sized and aligned for the actual instantiated class,
# which may be a subclass of id_.type (e.g. `cv.declare_id(BaseClass)`
# combined with `SubClass.new()` — used by ili9xxx, waveshare_epaper,
# etc. to select a model-specific constructor). Using id_.type would
# run the base-class default constructor instead, silently losing any
# subclass initialization. Template args live on the CallExpression
# and are re-emitted below.
call_expr = rhs.base
assert isinstance(call_expr, CallExpression), (
f"Expected CallExpression for placement new, got {type(call_expr)}"
)
actual_type = rhs.new_type if rhs.new_type is not None else id_.type
if call_expr.template_args is not None:
actual_type = f"{actual_type}{call_expr.template_args}"
pointer_type = id_.type
the_type = id_.type
# Extract component namespace from type for memory analysis attribution
component_ns = _extract_component_ns(str(actual_type))
component_ns = _extract_component_ns(str(the_type))
storage_name = f"{component_ns}__{id_.id}__pstorage"
# Declare aligned byte array for the object storage
CORE.add_global(
RawStatement(
f"alignas({actual_type}) static unsigned char {storage_name}[sizeof({actual_type})];"
f"alignas({the_type}) static unsigned char {storage_name}[sizeof({the_type})];"
)
)
# Pointer declaration uses id_.type to preserve the declared base-class
# pointer type for downstream callers (polymorphism through base ptr).
CORE.add_global(
AssignmentExpression(
f"static {pointer_type}",
f"static {the_type}",
"*const ",
id_,
MockObj(f"reinterpret_cast<{pointer_type} *>({storage_name})"),
MockObj(f"reinterpret_cast<{the_type} *>({storage_name})"),
)
)
placement_new = CallExpression(f"new({id_.id}) {actual_type}", *call_expr.args)
# Extract args from the CallExpression and rebuild as placement new.
# Template args are already encoded in the_type (e.g. GlobalsComponent<int>),
# so we only pass the constructor args, not template_args.
call_expr = rhs.base
assert isinstance(call_expr, CallExpression), (
f"Expected CallExpression for placement new, got {type(call_expr)}"
)
placement_new = CallExpression(f"new({id_.id}) {the_type}", *call_expr.args)
CORE.add(ExpressionStatement(placement_new))
else:
decl = VariableDeclarationExpression(id_.type, "*", id_, static=True)
@@ -879,16 +922,12 @@ class MockObj(Expression):
Mostly consists of magic methods that allow ESPHome's codegen syntax.
"""
__slots__ = ("base", "op", "is_new_expr", "new_type")
__slots__ = ("base", "op", "is_new_expr")
def __init__(self, base, op=".", is_new_expr=False, new_type=None) -> None:
def __init__(self, base, op=".", is_new_expr=False) -> None:
self.base = base
self.op = op
self.is_new_expr = is_new_expr
# For `is_new_expr=True` objects, `new_type` holds the class name being
# constructed (e.g. "ili9xxx::ILI9XXXST7789V"). Needed by Pvariable so
# placement new uses the actual subclass rather than id_.type.
self.new_type = new_type
def __getattr__(self, attr: str) -> "MockObj":
# prevent python dunder methods being replaced by mock objects
@@ -903,9 +942,7 @@ class MockObj(Expression):
def __call__(self, *args: SafeExpType) -> "MockObj":
call = CallExpression(self.base, *args)
return MockObj(
call, self.op, is_new_expr=self.is_new_expr, new_type=self.new_type
)
return MockObj(call, self.op, is_new_expr=self.is_new_expr)
def __str__(self):
return str(self.base)
@@ -919,7 +956,7 @@ class MockObj(Expression):
@property
def new(self) -> "MockObj":
return MockObj(f"new {self.base}", "->", is_new_expr=True, new_type=self.base)
return MockObj(f"new {self.base}", "->", is_new_expr=True)
def template(self, *args: SafeExpType) -> "MockObj":
"""Apply template parameters to this object."""

View File

@@ -3,8 +3,6 @@ dependencies:
version: "7.4.2"
esphome/esp-audio-libs:
version: 2.0.4
esphome/esp-micro-speech-features:
version: 1.2.3
esphome/micro-decoder:
version: 0.1.1
esphome/micro-flac:

View File

@@ -48,8 +48,6 @@ _SECRET_VALUES = {}
# Not thread-safe — config processing is single-threaded today.
_load_listeners: list[Callable[[Path], None]] = []
DocumentPath = list[str | int]
@contextmanager
def track_yaml_loads() -> Generator[list[Path]]:
@@ -681,123 +679,6 @@ def is_secret(value):
return None
def _path_doc(item: Any) -> str | None:
"""Return the source document name if *item* carries location info."""
if isinstance(item, ESPHomeDataBase) and (r := item.esp_range) is not None:
return r.start_mark.document
return None
def _fmt_mark(loc: Any) -> str:
"""Render a DocumentLocation as a 1-based 'file line:col' string."""
return f"{loc.document} {loc.line + 1}:{loc.column + 1}"
def _obj_loc(obj: Any) -> str:
"""Return formatted source location for *obj*, or '' if it has none."""
if isinstance(obj, ESPHomeDataBase) and (r := obj.esp_range) is not None:
return _fmt_mark(r.start_mark)
return ""
def _fmt_segment(seg: list) -> str:
"""Format a path segment, rendering integers as [n] subscripts."""
parts: list[str] = []
for item in seg:
if isinstance(item, int):
if parts:
parts[-1] = f"{parts[-1]}[{item}]"
else:
parts.append(f"[{item}]")
else:
parts.append(str(item))
return "->".join(parts)
def _split_into_frames(
path: DocumentPath,
) -> list[tuple[list, str]]:
"""Group *path* into per-file frames at include boundaries.
A "frame" is the slice of the path that belongs to one source document.
Each path item is either:
* a **located key** — has an ``ESPHomeDataBase`` source mark; this is
what tells us which document owns the surrounding keys.
* an **integer** — a list subscript; always attaches to the open frame
(renders as ``foo[3]`` on the previous name).
* an **unlocated string** — a key with no source mark (e.g. constants
like ``CONF_PACKAGES``); it describes the parent of the *next* file,
so it migrates to the next frame when the document changes.
Returns a list of ``(items, "file line:col")`` tuples in walk order
(outermost frame first).
"""
frames: list[tuple[list, str]] = []
open_frame: list = []
next_frame_keys: list = [] # unlocated strings buffered for the next frame
open_doc: str | None = None
open_loc = ""
for item in path:
doc = _path_doc(item)
if doc is None:
# Ints subscript the open frame's last name; everything else
# (strings, or leading ints with no open frame) is buffered for
# the next frame.
if isinstance(item, int) and open_doc is not None:
open_frame.append(item)
else:
next_frame_keys.append(item)
continue
if open_doc is not None and doc != open_doc:
# Crossed an include boundary: close the open frame.
frames.append((open_frame, open_loc))
open_frame = []
open_frame.extend(next_frame_keys)
next_frame_keys.clear()
open_frame.append(item)
open_doc = doc
open_loc = _fmt_mark(item.esp_range.start_mark)
if open_doc is not None:
# Trailing buffered keys belong to the innermost (last) frame.
open_frame.extend(next_frame_keys)
frames.append((open_frame, open_loc))
return frames
def format_path(path: DocumentPath, current_obj: Any) -> str:
"""Build a human-readable include stack from a config path.
Each YAML key in *path* that carries an ``ESPHomeDataBase`` ``esp_range``
reveals which file it came from. When the source document changes between
consecutive such keys, that is an include boundary. The path is split
into per-file frames and formatted innermost-first, e.g.::
In: packages->roam in common/package/wifi.yaml 26:10
Included from packages->net in common/hardware.yaml 44:2
Included from packages->device in my_project.yaml 11:2
The innermost ``In:`` line uses the location from *current_obj* when
available (the value that triggered the error) for extra precision.
"""
frames = _split_into_frames(path)
obj_loc = _obj_loc(current_obj)
if not frames:
# No source info anywhere in the path: render as a flat path,
# using current_obj's location if it happens to have one.
suffix = f" in {obj_loc}" if obj_loc else ""
return f"In: {_fmt_segment(path)}{suffix}"
inner_seg, inner_loc = frames[-1]
lines = [f"In: {_fmt_segment(inner_seg)} in {obj_loc or inner_loc}"]
for seg, loc in reversed(frames[:-1]):
lines.append(f" Included from {_fmt_segment(seg)} in {loc}")
return "\n".join(lines)
class ESPHomeDumper(yaml.SafeDumper):
def represent_mapping(self, tag, mapping, flow_style=None):
value = []

View File

@@ -155,6 +155,7 @@ lib_deps =
makuna/NeoPixelBus@2.8.0 ; neopixelbus
esphome/ESP32-audioI2S@2.3.0 ; i2s_audio
droscy/esp_wireguard@0.4.5 ; wireguard
kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word
build_flags =
${common:arduino.build_flags}
@@ -176,6 +177,7 @@ framework = espidf
lib_deps =
${common:idf.lib_deps}
droscy/esp_wireguard@0.4.5 ; wireguard
kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word
tonia/HeatpumpIR@1.0.41 ; heatpumpir
build_flags =
${common:idf.build_flags}

View File

@@ -6,13 +6,13 @@ colorama==0.4.6
icmplib==3.0.4
tornado==6.5.5
tzlocal==5.3.1 # from time
tzdata>=2026.1 # from time
tzdata>=2021.1 # from time
pyserial==3.5
platformio==6.1.19
esptool==5.2.0
click==8.3.2
esphome-dashboard==20260408.1
aioesphomeapi==44.18.0
aioesphomeapi==44.16.0
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.19.1 # dashboard_import
@@ -27,7 +27,7 @@ smpclient==6.0.0
requests==2.33.1
# esp-idf >= 5.0 requires this
pyparsing >= 3.3.2
pyparsing >= 3.0
# For autocompletion
argcomplete>=2.0.0

View File

@@ -722,22 +722,18 @@ def lint_trailing_whitespace(fname, match):
# Heap-allocating helpers that cause fragmentation on long-running embedded devices.
# These return std::string and should be replaced with stack-based alternatives.
HEAP_ALLOCATING_HELPERS = {
"base64_encode": "base64_encode_to() with a pre-allocated buffer",
"format_bin": "format_bin_to() with a stack buffer",
"format_hex": "format_hex_to() with a stack buffer",
"format_hex_pretty": "format_hex_pretty_to() with a stack buffer",
"format_mac_address_pretty": "format_mac_addr_upper() with a stack buffer",
"get_mac_address": "get_mac_address_into_buffer() with a stack buffer",
"get_mac_address_pretty": "get_mac_address_pretty_into_buffer() with a stack buffer",
"str_lower_case": "manual tolower() with a stack buffer",
"str_sanitize": "str_sanitize_to() with a stack buffer",
"str_truncate": "removal (function is unused)",
"str_until": "manual strchr()/find() with a StringRef or stack buffer",
"str_upper_case": "removal (function is unused)",
"str_snake_case": "removal (function is unused)",
"str_sprintf": "snprintf() with a stack buffer",
"str_snprintf": "snprintf() with a stack buffer",
"value_accuracy_to_string": "value_accuracy_to_buf() with a stack buffer",
}
@@ -747,33 +743,24 @@ HEAP_ALLOCATING_HELPERS = {
# get_mac_address(?!_) ensures we don't match get_mac_address_into_buffer, etc.
# CPP_RE_EOL captures rest of line so NOLINT comments are detected
r"[^\w]("
r"base64_encode(?!_)|"
r"format_bin(?!_)|"
r"format_hex(?!_)|"
r"format_hex_pretty(?!_)|"
r"format_mac_address_pretty|"
r"get_mac_address_pretty(?!_)|"
r"get_mac_address(?!_)|"
r"str_lower_case|"
r"str_sanitize(?!_)|"
r"str_truncate|"
r"str_until|"
r"str_upper_case|"
r"str_snake_case|"
r"str_sprintf|"
r"str_snprintf|"
r"value_accuracy_to_string"
r"str_snprintf"
r")\s*\(" + CPP_RE_EOL,
include=cpp_include,
exclude=[
# The definitions themselves
"esphome/core/alloc_helpers.h",
"esphome/core/alloc_helpers.cpp",
# Backward compatibility re-exports (remove before 2026.11.0)
"esphome/core/helpers.h",
"esphome/core/helpers.cpp",
# Vendored third-party library
"esphome/components/http_request/httplib.h",
],
)
def lint_no_heap_allocating_helpers(fname, match):
@@ -825,7 +812,6 @@ def lint_no_sprintf(fname, match):
"esphome/components/http_request/httplib.h",
# Deprecated helpers that return std::string
"esphome/core/helpers.cpp",
"esphome/core/alloc_helpers.cpp",
# The using declaration itself
"esphome/core/helpers.h",
# Test fixtures - not production embedded code

View File

@@ -1,8 +1,6 @@
from __future__ import annotations
import ast
from collections.abc import Callable
from dataclasses import dataclass, field
from functools import cache
import hashlib
import json
@@ -141,109 +139,6 @@ def get_component_test_files(
return list(tests_dir.glob("test.*.yaml"))
@dataclass(frozen=True)
class ComponentMetadata:
"""Statically-parsed AUTO_LOAD and CONFLICTS_WITH declarations."""
auto_load: frozenset[str] = field(default_factory=frozenset)
conflicts_with: frozenset[str] = field(default_factory=frozenset)
@cache
def parse_component_metadata(name: str) -> ComponentMetadata:
"""Return the AUTO_LOAD / CONFLICTS_WITH declarations for a component.
Parses the component's ``esphome/components/<name>/__init__.py`` statically.
Callable forms (``def AUTO_LOAD():``) require runtime imports and are
reported as empty -- safe for conflict detection since they cannot be
evaluated without executing the module.
"""
init_file = Path(root_path) / ESPHOME_COMPONENTS_PATH / name / "__init__.py"
if not init_file.exists():
return ComponentMetadata()
try:
tree = ast.parse(init_file.read_text(encoding="utf-8"))
except (OSError, SyntaxError, UnicodeError):
return ComponentMetadata()
fields: dict[str, frozenset[str]] = {
"AUTO_LOAD": frozenset(),
"CONFLICTS_WITH": frozenset(),
}
for node in tree.body:
if not isinstance(node, ast.Assign) or not isinstance(node.value, ast.List):
continue
for target in node.targets:
if not isinstance(target, ast.Name) or target.id not in fields:
continue
fields[target.id] = frozenset(
e.value
for e in node.value.elts
if isinstance(e, ast.Constant) and isinstance(e.value, str)
)
return ComponentMetadata(
auto_load=fields["AUTO_LOAD"],
conflicts_with=fields["CONFLICTS_WITH"],
)
@dataclass
class _ConflictWalk:
loaded: set[str]
rejects: set[str]
def split_conflicting_groups(
grouped_components: dict[tuple[str, str], list[str]],
) -> dict[tuple[str, str], list[str]]:
"""Split groups so components declaring mutual CONFLICTS_WITH end up in separate builds.
A conflict propagates through AUTO_LOAD: if X declares CONFLICTS_WITH=[Y]
and Z auto-loads Y, then X and Z conflict (e.g. bme680_bsec vs.
bme68x_bsec2_i2c which auto-loads bme68x_bsec2). Only components that
appear in the batch (and their AUTO_LOAD closures) are parsed. The
conflict relation is treated as symmetric even when only one side
declares it (e.g. ethernet rejects wifi but wifi does not declare the
reverse).
"""
batch = {c for comps in grouped_components.values() for c in comps}
walks: dict[str, _ConflictWalk] = {}
for comp in batch:
walk = _ConflictWalk(loaded={comp}, rejects=set())
stack = [comp]
while stack:
metadata = parse_component_metadata(stack.pop())
walk.rejects |= metadata.conflicts_with
new = metadata.auto_load - walk.loaded
walk.loaded |= new
stack.extend(new)
walks[comp] = walk
def conflicts(a: str, b: str) -> bool:
wa, wb = walks[a], walks[b]
return not wa.rejects.isdisjoint(wb.loaded) or not wb.rejects.isdisjoint(
wa.loaded
)
result: dict[tuple[str, str], list[str]] = {}
for (platform, signature), components in grouped_components.items():
buckets: list[list[str]] = []
for comp in components:
for bucket in buckets:
if not any(conflicts(comp, other) for other in bucket):
bucket.append(comp)
break
else:
buckets.append([comp])
if len(buckets) == 1:
result[(platform, signature)] = buckets[0]
continue
for index, bucket in enumerate(buckets):
key = signature if index == 0 else f"{signature}__conflict{index}"
result[(platform, key)] = bucket
return result
def styled(color: str | tuple[str, ...], msg: str, reset: bool = True) -> str:
prefix = "".join(color) if isinstance(color, tuple) else color
suffix = colorama.Style.RESET_ALL if reset else ""

View File

@@ -28,7 +28,7 @@ from script.analyze_component_buses import (
create_grouping_signature,
merge_compatible_bus_groups,
)
from script.helpers import get_component_test_files, split_conflicting_groups
from script.helpers import get_component_test_files
# Weighting for batch creation
# Isolated components can't be grouped/merged, so they count as 10x
@@ -145,11 +145,6 @@ def create_intelligent_batches(
# improving the efficiency of test_build_components.py grouping
signature_groups = merge_compatible_bus_groups(signature_groups)
# Split groups containing mutually-incompatible components (CONFLICTS_WITH).
# Without this, batch weighting assumes the group is one build when it will
# actually be split into two at build time -- throwing off CI distribution.
signature_groups = split_conflicting_groups(signature_groups)
# Create batches by keeping signature groups together
# Components with the same signature stay in the same batches
batches = []

View File

@@ -39,7 +39,7 @@ from script.analyze_component_buses import (
merge_compatible_bus_groups,
uses_local_file_references,
)
from script.helpers import get_component_test_files, split_conflicting_groups
from script.helpers import get_component_test_files
from script.merge_component_configs import merge_component_configs
@@ -675,13 +675,6 @@ def run_grouped_component_tests(
# as long as they don't have conflicting configurations for the same bus type
grouped_components = merge_compatible_bus_groups(grouped_components)
# Split groups that contain components declaring CONFLICTS_WITH each other.
# The bus-level merge above only considers shared bus configs; components
# with the same bus signature (e.g. both I2C) can still be mutually
# incompatible (e.g. bme680_bsec vs. bme68x_bsec2_i2c which auto-loads
# bme68x_bsec2). Those must end up in separate builds.
grouped_components = split_conflicting_groups(grouped_components)
# Print detailed grouping plan
print("\nGrouping Plan:")
print("-" * 80)

View File

@@ -8,16 +8,10 @@ from typing import Any
import pytest
from esphome.components.esp32 import VARIANT_ESP32, VARIANTS
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT
from esphome.components.esp32.gpio import validate_gpio_pin
from esphome.components.esp32 import VARIANTS
from esphome.components.esp32.const import KEY_ESP32, KEY_SDKCONFIG_OPTIONS
import esphome.config_validation as cv
from esphome.const import (
CONF_ESPHOME,
CONF_IGNORE_PIN_VALIDATION_ERROR,
CONF_NUMBER,
PlatformFramework,
)
from esphome.const import CONF_ESPHOME, PlatformFramework
from esphome.core import CORE
from tests.component_tests.types import SetCoreConfigCallable
@@ -155,73 +149,6 @@ def test_execute_from_psram_p4_sdkconfig(
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
def test_ignore_pin_validation_error_on_clean_pin_warns(
set_core_config: SetCoreConfigCallable,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A pin that passes validation but sets `ignore_pin_validation_error: true`
should log a warning nudging the user to remove the flag, and not raise."""
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
)
pin = {CONF_NUMBER: 4, CONF_IGNORE_PIN_VALIDATION_ERROR: True}
with caplog.at_level("WARNING"):
result = validate_gpio_pin(pin)
assert result[CONF_NUMBER] == 4
assert "GPIO4 has no validation errors to ignore" in caplog.text
def test_ignore_pin_validation_error_on_dirty_pin_suppresses(
set_core_config: SetCoreConfigCallable,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A pin that fails validation with `ignore_pin_validation_error: true` should
log the suppression warning and not raise (existing behavior)."""
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
)
# GPIO6 is a flash pin on ESP32 -> pin_validation raises cv.Invalid
pin = {CONF_NUMBER: 6, CONF_IGNORE_PIN_VALIDATION_ERROR: True}
with caplog.at_level("WARNING"):
result = validate_gpio_pin(pin)
assert result[CONF_NUMBER] == 6
assert "Ignoring validation error on pin 6" in caplog.text
def test_dirty_pin_without_ignore_flag_raises(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A pin that fails validation without the ignore flag should still raise."""
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
)
pin = {CONF_NUMBER: 6, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
with pytest.raises(cv.Invalid, match="flash interface"):
validate_gpio_pin(pin)
def test_clean_pin_without_ignore_flag_does_not_warn(
set_core_config: SetCoreConfigCallable,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A clean pin without the ignore flag should pass silently."""
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
)
pin = {CONF_NUMBER: 4, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
with caplog.at_level("WARNING"):
result = validate_gpio_pin(pin)
assert result[CONF_NUMBER] == 4
assert "has no validation errors to ignore" not in caplog.text
def test_execute_from_psram_disabled_sdkconfig(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],

View File

@@ -1,20 +0,0 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: arduino
spi:
clk_pin: GPIO18
mosi_pin: GPIO23
display:
- platform: ili9xxx
id: tft_display
model: ST7789V
cs_pin: GPIO5
dc_pin: GPIO17
reset_pin: GPIO16
invert_colors: false

View File

@@ -1,31 +0,0 @@
"""Tests for the ili9xxx component."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
def test_ili9xxx_placement_new_uses_model_subclass(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Regression test for ili9xxx picking the right constructor under placement new.
ili9xxx declares the ID as the base ``ILI9XXXDisplay`` but constructs a
model-specific subclass (e.g. ``ILI9XXXST7789V``) via ``MODELS[...].new()``.
Pvariable must emit placement new for the subclass — otherwise the base
default constructor runs and the panel is left with a null init sequence
and 0x0 dimensions, producing a silent blank screen.
"""
main_cpp = generate_main(component_config_path("ili9xxx_test.yaml"))
# Storage is sized for the subclass so the full object fits.
assert "sizeof(ili9xxx::ILI9XXXST7789V)" in main_cpp
assert "alignas(ili9xxx::ILI9XXXST7789V)" in main_cpp
# Pointer is declared as the base type for polymorphism.
assert "static ili9xxx::ILI9XXXDisplay *const tft_display" in main_cpp
# Placement new runs the subclass constructor — this is the actual regression fix.
assert "new(tft_display) ili9xxx::ILI9XXXST7789V()" in main_cpp
# Base-class default constructor must NOT be used.
assert "new(tft_display) ili9xxx::ILI9XXXDisplay()" not in main_cpp

View File

@@ -7,11 +7,6 @@ import pytest
from esphome import config_validation as cv
from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32P4
# Importing xl9535 registers its pin schema with pins.PIN_SCHEMA_REGISTRY so that
# models (e.g. SEEED-RETERMINAL-D1001) that reference xl9535-backed pins in their
# defaults can be validated by the mipi_dsi CONFIG_SCHEMA in this test.
import esphome.components.xl9535 # noqa: F401
from esphome.const import (
CONF_DIMENSIONS,
CONF_HEIGHT,

View File

@@ -2,20 +2,18 @@
import logging
from pathlib import Path
import re
from unittest.mock import MagicMock, patch
import pytest
from esphome.components.packages import (
CONFIG_SCHEMA,
_substitute_package_definition,
_walk_packages,
do_packages_pass,
is_package_definition,
merge_packages,
)
from esphome.components.substitutions import ContextVars, do_substitution_pass
from esphome.components.substitutions import do_substitution_pass
import esphome.config as config_module
from esphome.config import resolve_extend_remove
from esphome.config_helpers import Extend, Remove
@@ -46,7 +44,7 @@ from esphome.const import (
)
from esphome.core import CORE
from esphome.util import OrderedDict
from esphome.yaml_util import DocumentPath, IncludeFile, add_context, load_yaml
from esphome.yaml_util import IncludeFile, add_context
# Test strings
TEST_DEVICE_NAME = "test_device_name"
@@ -1113,7 +1111,7 @@ def test_packages_include_file_resolves_to_list(mock_resolve_include) -> None:
"""When packages: is an IncludeFile that resolves to a list, it is processed correctly."""
include_file = MagicMock(spec=IncludeFile)
package_content = {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}}
mock_resolve_include.return_value = [package_content]
mock_resolve_include.return_value = ([package_content], None)
config = {CONF_PACKAGES: include_file}
result = do_packages_pass(config)
@@ -1127,7 +1125,7 @@ def test_packages_include_file_resolves_to_dict(mock_resolve_include) -> None:
"""When packages: is an IncludeFile that resolves to a dict, it is processed correctly."""
include_file = MagicMock(spec=IncludeFile)
package_content = {CONF_WIFI: {CONF_SSID: TEST_PACKAGE_WIFI_SSID}}
mock_resolve_include.return_value = {"network": package_content}
mock_resolve_include.return_value = ({"network": package_content}, None)
config = {CONF_PACKAGES: include_file}
result = do_packages_pass(config)
@@ -1142,7 +1140,7 @@ def test_packages_include_file_resolves_to_invalid_type_raises(
) -> None:
"""When packages: is an IncludeFile that resolves to an invalid type, cv.Invalid is raised."""
include_file = MagicMock(spec=IncludeFile)
mock_resolve_include.return_value = "not_a_dict_or_list"
mock_resolve_include.return_value = ("not_a_dict_or_list", None)
config = {CONF_PACKAGES: include_file}
with pytest.raises(
@@ -1215,9 +1213,7 @@ def test_named_dict_with_include_files_no_false_deprecation_warning(
call_count = 0
def failing_callback(
package_config: dict, context: object, path: DocumentPath | None = None
) -> dict:
def failing_callback(package_config: dict, context: object) -> dict:
nonlocal call_count
call_count += 1
if call_count == 1:
@@ -1253,9 +1249,7 @@ def test_validate_deprecated_false_raises_directly(
call_count = 0
def failing_callback(
package_config: dict, context: object, path: DocumentPath | None = None
) -> dict:
def failing_callback(package_config: dict, context: object) -> dict:
nonlocal call_count
call_count += 1
if call_count == 1:
@@ -1287,9 +1281,7 @@ def test_error_on_first_declared_package_still_detected() -> None:
call_count = 0
def fail_on_last(
package_config: dict, context: object, path: DocumentPath | None = None
) -> dict:
def fail_on_last(package_config: dict, context: object) -> dict:
nonlocal call_count
call_count += 1
# Reverse iteration: third_pkg (1), second_pkg (2), first_pkg (3)
@@ -1318,9 +1310,7 @@ def test_deprecated_single_package_fallback_still_works(
attempt = 0
def fail_then_succeed(
package_config: dict, context: object, path: DocumentPath | None = None
) -> dict:
def fail_then_succeed(package_config: dict, context: object) -> dict:
nonlocal attempt
attempt += 1
if attempt == 1:
@@ -1409,85 +1399,3 @@ def test_raw_config_contains_merged_esphome_from_package(tmp_path) -> None:
"CORE.raw_config should contain esphome section after package merge"
)
assert CORE.raw_config[CONF_ESPHOME][CONF_NAME] == TEST_DEVICE_NAME
# ---------------------------------------------------------------------------
# _substitute_package_definition
# ---------------------------------------------------------------------------
def test_substitute_package_definition_local_dict_returned_unchanged() -> None:
"""A plain local config dict is not substituted and is returned as-is."""
pkg = {CONF_WIFI: {CONF_SSID: "test"}}
result = _substitute_package_definition(pkg, ContextVars())
assert result is pkg
def test_substitute_package_definition_string_resolved_with_context() -> None:
"""A string package definition has its variables substituted."""
ctx = ContextVars({"variant": "esp32"})
result = _substitute_package_definition("device-${variant}.yaml", ctx)
assert result == "device-esp32.yaml"
def test_substitute_package_definition_undefined_in_string() -> None:
"""An undefined variable in a package URL string raises cv.Invalid."""
with pytest.raises(cv.Invalid, match="Undefined variable in package definition"):
_substitute_package_definition(
"github://org/repo/${undefined_var}/pkg.yaml", ContextVars()
)
def test_substitute_package_definition_undefined_in_remote_dict_field() -> None:
"""An undefined variable inside a remote-dict field names the offending field."""
with pytest.raises(cv.Invalid) as exc_info:
_substitute_package_definition(
{CONF_URL: "github://${typo}/repo"}, ContextVars()
)
err = str(exc_info.value)
assert "'typo' is undefined" in err
assert CONF_URL in err
def test_substitute_package_definition_undefined_in_remote_dict_non_first_field() -> (
None
):
"""The field path joins correctly for non-first dict fields (e.g. ``ref``)."""
with pytest.raises(cv.Invalid) as exc_info:
_substitute_package_definition(
{
CONF_URL: "github://org/repo",
CONF_REF: "branch-${branch_typo}",
},
ContextVars(),
)
err = str(exc_info.value)
assert "'branch_typo' is undefined" in err
assert CONF_REF in err
def test_substitute_package_definition_includes_source_location(tmp_path: Path) -> None:
"""A package loaded from YAML surfaces file/line/col in the cv.Invalid message.
Line/column are rendered 1-based (matching config.line_info() and editor
line numbering) and point at the offending scalar, not the enclosing dict.
"""
yaml_file = tmp_path / "main.yaml"
yaml_file.write_text(
"packages:\n broken: github://org/repo/${undefined_var}/pkg.yaml\n"
)
config = load_yaml(yaml_file)
package_config = config[CONF_PACKAGES]["broken"]
with pytest.raises(cv.Invalid) as exc_info:
_substitute_package_definition(package_config, ContextVars())
err = str(exc_info.value)
assert "main.yaml" in err
# The offending value lives on line 2 (1-based). Column depends on the YAML
# loader, so we only pin line and check that a 1-based column is present.
match = re.search(r"main\.yaml (\d+):(\d+)", err)
assert match, err
line, col = int(match.group(1)), int(match.group(2))
assert line == 2, f"expected 1-based line 2, got {line} (err={err!r})"
assert col >= 1, f"expected 1-based column ≥ 1, got {col} (err={err!r})"

View File

@@ -1,14 +0,0 @@
esphome:
name: test
host:
text:
- platform: template
name: "Test Text Restore"
id: test_text_restore
optimistic: true
max_length: 10
mode: text
initial_value: "hello"
restore_value: true

Some files were not shown because too many files have changed in this diff Show More