mirror of
https://github.com/esphome/esphome.git
synced 2026-09-02 11:06:04 +00:00
Merge branch 'dev' into api-stringref-user-services
This commit is contained in:
+16
-3
@@ -431,6 +431,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
return 1
|
||||
_LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate)
|
||||
|
||||
process_stacktrace = None
|
||||
|
||||
try:
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
process_stacktrace = getattr(module, "process_stacktrace")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
backtrace_state = False
|
||||
ser = serial.Serial()
|
||||
ser.baudrate = baud_rate
|
||||
@@ -472,9 +480,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
)
|
||||
safe_print(parser.parse_line(line, time_str))
|
||||
|
||||
backtrace_state = platformio_api.process_stacktrace(
|
||||
config, line, backtrace_state=backtrace_state
|
||||
)
|
||||
if process_stacktrace:
|
||||
backtrace_state = process_stacktrace(
|
||||
config, line, backtrace_state
|
||||
)
|
||||
else:
|
||||
backtrace_state = platformio_api.process_stacktrace(
|
||||
config, line, backtrace_state=backtrace_state
|
||||
)
|
||||
except serial.SerialException:
|
||||
_LOGGER.error("Serial port closed!")
|
||||
return 0
|
||||
|
||||
@@ -36,6 +36,8 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
||||
static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str()
|
||||
static std::string value_to_string(const std::string &val) { return val; }
|
||||
static std::string value_to_string(std::string &&val) { return std::move(val); }
|
||||
static std::string value_to_string(const StringRef &val) { return val.str(); }
|
||||
static std::string value_to_string(StringRef &&val) { return val.str(); }
|
||||
|
||||
public:
|
||||
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
|
||||
|
||||
@@ -29,10 +29,10 @@ enum class CleaningState : uint8_t {
|
||||
enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER };
|
||||
|
||||
struct HonSettings {
|
||||
hon_protocol::VerticalSwingMode last_vertiacal_swing;
|
||||
hon_protocol::HorizontalSwingMode last_horizontal_swing;
|
||||
bool beeper_state;
|
||||
bool quiet_mode_state;
|
||||
hon_protocol::VerticalSwingMode last_vertiacal_swing{hon_protocol::VerticalSwingMode::CENTER};
|
||||
hon_protocol::HorizontalSwingMode last_horizontal_swing{hon_protocol::HorizontalSwingMode::CENTER};
|
||||
bool beeper_state{true};
|
||||
bool quiet_mode_state{false};
|
||||
};
|
||||
|
||||
class HonClimate : public HaierClimateBase {
|
||||
@@ -189,7 +189,7 @@ class HonClimate : public HaierClimateBase {
|
||||
int big_data_sensors_{0};
|
||||
esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
|
||||
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
|
||||
HonSettings settings_;
|
||||
HonSettings settings_{};
|
||||
ESPPreferenceObject hon_rtc_;
|
||||
SwitchState quiet_mode_state_{SwitchState::OFF};
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -380,3 +382,41 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
|
||||
asyncio.run(logger_connect(address))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _addr2line(addr2line: str, elf: Path, addr: str) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[addr2line, "-e", elf, addr],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip().splitlines()[0]
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.error("Running command failed: %s", err)
|
||||
return ""
|
||||
|
||||
|
||||
def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) -> bool:
|
||||
if "Last crash:" in line:
|
||||
return True
|
||||
if backtrace_state:
|
||||
match = re.search(r"PC=(0x[0-9a-fA-F]+)\s+LR=(0x[0-9a-fA-F]+)", line)
|
||||
if match:
|
||||
pc = match.group(1)
|
||||
lr = match.group(2)
|
||||
from esphome.analyze_memory.toolchain import find_tool
|
||||
|
||||
addr2line = find_tool("addr2line")
|
||||
if addr2line is None:
|
||||
return False
|
||||
elf = CORE.relative_pioenvs_path(CORE.name, "firmware.elf")
|
||||
if not elf.exists():
|
||||
_LOGGER.warning("%s does not exists", elf)
|
||||
return False
|
||||
_LOGGER.error("=== CRASH ===")
|
||||
_LOGGER.error("PC: %s", _addr2line(addr2line, elf, pc))
|
||||
_LOGGER.error("LR: %s", _addr2line(addr2line, elf, lr))
|
||||
|
||||
return False
|
||||
|
||||
@@ -406,7 +406,7 @@ void Scheduler::full_cleanup_removed_items_() {
|
||||
// Compact in-place: move valid items forward, recycle removed ones
|
||||
size_t write = 0;
|
||||
for (size_t read = 0; read < this->items_.size(); ++read) {
|
||||
if (!is_item_removed_(this->items_[read].get())) {
|
||||
if (!is_item_removed_locked_(this->items_[read].get())) {
|
||||
if (write != read) {
|
||||
this->items_[write] = std::move(this->items_[read]);
|
||||
}
|
||||
@@ -421,6 +421,29 @@ void Scheduler::full_cleanup_removed_items_() {
|
||||
this->to_remove_ = 0;
|
||||
}
|
||||
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
void Scheduler::compact_defer_queue_locked_() {
|
||||
// Rare case: new items were added during processing - compact the vector
|
||||
// This only happens when:
|
||||
// 1. A deferred callback calls defer() again, or
|
||||
// 2. Another thread calls defer() while we're processing
|
||||
//
|
||||
// Move unprocessed items (added during this loop) to the front for next iteration
|
||||
//
|
||||
// SAFETY: Compacted items may include cancelled items (marked for removal via
|
||||
// cancel_item_locked_() during execution). This is safe because should_skip_item_()
|
||||
// checks is_item_removed_() before executing, so cancelled items will be skipped
|
||||
// and recycled on the next loop iteration.
|
||||
size_t remaining = this->defer_queue_.size() - this->defer_queue_front_;
|
||||
for (size_t i = 0; i < remaining; i++) {
|
||||
this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]);
|
||||
}
|
||||
// Use erase() instead of resize() to avoid instantiating _M_default_append
|
||||
// (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed.
|
||||
this->defer_queue_.erase(this->defer_queue_.begin() + remaining, this->defer_queue_.end());
|
||||
}
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
|
||||
void HOT Scheduler::call(uint32_t now) {
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
this->process_defer_queue_(now);
|
||||
@@ -508,7 +531,7 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
// Multi-threaded platforms without atomics: must take lock to safely read remove flag
|
||||
{
|
||||
LockGuard guard{this->lock_};
|
||||
if (is_item_removed_(item.get())) {
|
||||
if (is_item_removed_locked_(item.get())) {
|
||||
this->recycle_item_main_loop_(this->pop_raw_locked_());
|
||||
this->to_remove_--;
|
||||
continue;
|
||||
@@ -545,7 +568,7 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
// during the function call and know if we were cancelled.
|
||||
auto executed_item = this->pop_raw_locked_();
|
||||
|
||||
if (executed_item->remove) {
|
||||
if (this->is_item_removed_locked_(executed_item.get())) {
|
||||
// We were removed/cancelled in the function call, recycle and continue
|
||||
this->to_remove_--;
|
||||
this->recycle_item_main_loop_(std::move(executed_item));
|
||||
@@ -572,7 +595,7 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
void HOT Scheduler::process_to_add() {
|
||||
LockGuard guard{this->lock_};
|
||||
for (auto &it : this->to_add_) {
|
||||
if (is_item_removed_(it.get())) {
|
||||
if (is_item_removed_locked_(it.get())) {
|
||||
// Recycle cancelled items
|
||||
this->recycle_item_main_loop_(std::move(it));
|
||||
continue;
|
||||
@@ -605,7 +628,7 @@ size_t HOT Scheduler::cleanup_() {
|
||||
LockGuard guard{this->lock_};
|
||||
while (!this->items_.empty()) {
|
||||
auto &item = this->items_[0];
|
||||
if (!item->remove)
|
||||
if (!this->is_item_removed_locked_(item.get()))
|
||||
break;
|
||||
this->to_remove_--;
|
||||
this->recycle_item_main_loop_(this->pop_raw_locked_());
|
||||
|
||||
+52
-45
@@ -314,8 +314,8 @@ class Scheduler {
|
||||
// Fixes: https://github.com/esphome/esphome/issues/11940
|
||||
if (!item)
|
||||
return false;
|
||||
if (item->component != component || item->type != type || (skip_removed && item->remove) ||
|
||||
(match_retry && !item->is_retry)) {
|
||||
if (item->component != component || item->type != type ||
|
||||
(skip_removed && this->is_item_removed_locked_(item.get())) || (match_retry && !item->is_retry)) {
|
||||
return false;
|
||||
}
|
||||
// Name type must match
|
||||
@@ -387,41 +387,46 @@ class Scheduler {
|
||||
// No lock needed: single consumer (main loop), stale read just means we process less this iteration
|
||||
size_t defer_queue_end = this->defer_queue_.size();
|
||||
|
||||
// Fast path: nothing to process, avoid lock entirely.
|
||||
// Safe without lock: single consumer (main loop) reads front_, and a stale size() read
|
||||
// from a concurrent push can only make us see fewer items — they'll be processed next loop.
|
||||
if (this->defer_queue_front_ >= defer_queue_end)
|
||||
return;
|
||||
|
||||
// Merge lock acquisitions: instead of separate locks for move-out and recycle (2N+1 total),
|
||||
// recycle each item after re-acquiring the lock for the next iteration (N+1 total).
|
||||
// The lock is held across: recycle → loop condition → move-out, then released for execution.
|
||||
std::unique_ptr<SchedulerItem> item;
|
||||
|
||||
this->lock_.lock();
|
||||
while (this->defer_queue_front_ < defer_queue_end) {
|
||||
std::unique_ptr<SchedulerItem> item;
|
||||
{
|
||||
LockGuard lock(this->lock_);
|
||||
// SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_.
|
||||
// This is intentional and safe because:
|
||||
// 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function
|
||||
// 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_
|
||||
// and has_cancelled_timeout_in_container_locked_ in scheduler.h)
|
||||
// 3. The lock protects concurrent access, but the nullptr remains until cleanup
|
||||
item = std::move(this->defer_queue_[this->defer_queue_front_]);
|
||||
this->defer_queue_front_++;
|
||||
}
|
||||
// SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_.
|
||||
// This is intentional and safe because:
|
||||
// 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function
|
||||
// 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_
|
||||
// and has_cancelled_timeout_in_container_locked_ in scheduler.h)
|
||||
// 3. The lock protects concurrent access, but the nullptr remains until cleanup
|
||||
item = std::move(this->defer_queue_[this->defer_queue_front_]);
|
||||
this->defer_queue_front_++;
|
||||
this->lock_.unlock();
|
||||
|
||||
// Execute callback without holding lock to prevent deadlocks
|
||||
// if the callback tries to call defer() again
|
||||
if (!this->should_skip_item_(item.get())) {
|
||||
now = this->execute_item_(item.get(), now);
|
||||
}
|
||||
// Recycle the defer item after execution
|
||||
{
|
||||
LockGuard lock(this->lock_);
|
||||
this->recycle_item_main_loop_(std::move(item));
|
||||
}
|
||||
}
|
||||
|
||||
// If we've consumed all items up to the snapshot point, clean up the dead space
|
||||
// Single consumer (main loop), so no lock needed for this check
|
||||
if (this->defer_queue_front_ >= defer_queue_end) {
|
||||
LockGuard lock(this->lock_);
|
||||
this->cleanup_defer_queue_locked_();
|
||||
this->lock_.lock();
|
||||
this->recycle_item_main_loop_(std::move(item));
|
||||
}
|
||||
// Clean up the queue (lock already held from last recycle or initial acquisition)
|
||||
this->cleanup_defer_queue_locked_();
|
||||
this->lock_.unlock();
|
||||
}
|
||||
|
||||
// Helper to cleanup defer_queue_ after processing
|
||||
// Helper to cleanup defer_queue_ after processing.
|
||||
// Keeps the common clear() path inline, outlines the rare compaction to keep
|
||||
// cold code out of the hot instruction cache lines.
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
inline void cleanup_defer_queue_locked_() {
|
||||
// Check if new items were added by producers during processing
|
||||
@@ -429,27 +434,17 @@ class Scheduler {
|
||||
// Common case: no new items - clear everything
|
||||
this->defer_queue_.clear();
|
||||
} else {
|
||||
// Rare case: new items were added during processing - compact the vector
|
||||
// This only happens when:
|
||||
// 1. A deferred callback calls defer() again, or
|
||||
// 2. Another thread calls defer() while we're processing
|
||||
//
|
||||
// Move unprocessed items (added during this loop) to the front for next iteration
|
||||
//
|
||||
// SAFETY: Compacted items may include cancelled items (marked for removal via
|
||||
// cancel_item_locked_() during execution). This is safe because should_skip_item_()
|
||||
// checks is_item_removed_() before executing, so cancelled items will be skipped
|
||||
// and recycled on the next loop iteration.
|
||||
size_t remaining = this->defer_queue_.size() - this->defer_queue_front_;
|
||||
for (size_t i = 0; i < remaining; i++) {
|
||||
this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]);
|
||||
}
|
||||
// Use erase() instead of resize() to avoid instantiating _M_default_append
|
||||
// (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed.
|
||||
this->defer_queue_.erase(this->defer_queue_.begin() + remaining, this->defer_queue_.end());
|
||||
// Rare case: new items were added during processing - outlined to keep cold code
|
||||
// out of the hot instruction cache lines
|
||||
this->compact_defer_queue_locked_();
|
||||
}
|
||||
this->defer_queue_front_ = 0;
|
||||
}
|
||||
|
||||
// Cold path for compacting defer_queue_ when new items were added during processing.
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
// IMPORTANT: Must not be inlined - rare path, outlined to keep it out of the hot instruction cache lines.
|
||||
void __attribute__((noinline)) compact_defer_queue_locked_();
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
|
||||
// Helper to check if item is marked for removal (platform-specific)
|
||||
@@ -468,6 +463,18 @@ class Scheduler {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Helper to check if item is marked for removal when lock is already held.
|
||||
// Uses relaxed ordering since the mutex provides all necessary synchronization.
|
||||
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
|
||||
bool is_item_removed_locked_(SchedulerItem *item) const {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
// Lock already held - relaxed is sufficient, mutex provides ordering
|
||||
return item->remove.load(std::memory_order_relaxed);
|
||||
#else
|
||||
return item->remove;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Helper to set item removal flag (platform-specific)
|
||||
// For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
|
||||
// function. Uses memory_order_release when setting to true (for cancellation synchronization),
|
||||
@@ -524,7 +531,7 @@ class Scheduler {
|
||||
// it will iterate over these nullptr items. This check prevents crashes.
|
||||
if (!item)
|
||||
continue;
|
||||
if (is_item_removed_(item.get()) &&
|
||||
if (this->is_item_removed_locked_(item.get()) &&
|
||||
this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, SchedulerItem::TIMEOUT,
|
||||
match_retry, /* skip_removed= */ false)) {
|
||||
return true;
|
||||
|
||||
@@ -494,6 +494,22 @@ def lint_no_byte_datatype(fname, match):
|
||||
)
|
||||
|
||||
|
||||
@lint_re_check(
|
||||
r"(?:std\s*::\s*string_view|#include\s*<string_view>)" + CPP_RE_EOL,
|
||||
include=cpp_include,
|
||||
)
|
||||
def lint_no_std_string_view(fname, match):
|
||||
return (
|
||||
f"{highlight('std::string_view')} is not allowed in ESPHome. "
|
||||
f"It pulls in significant STL template machinery that bloats flash on "
|
||||
f"resource-constrained embedded targets, does not work well with ArduinoJson, "
|
||||
f"and duplicates functionality already provided by {highlight('StringRef')}.\n"
|
||||
f"Please use {highlight('StringRef')} from {highlight('esphome/core/string_ref.h')} "
|
||||
f"for non-owning string references, or {highlight('const char *')} for simple cases.\n"
|
||||
f"(If strictly necessary, add `{highlight('// NOLINT')}` to the end of the line)"
|
||||
)
|
||||
|
||||
|
||||
@lint_post_check
|
||||
def lint_constants_usage():
|
||||
errs = []
|
||||
|
||||
@@ -90,6 +90,19 @@ text_sensor:
|
||||
id: ha_hello_world_text2
|
||||
attribute: some_attribute
|
||||
|
||||
event:
|
||||
- platform: template
|
||||
name: Test Event
|
||||
id: test_event
|
||||
event_types:
|
||||
- test_event_type
|
||||
on_event:
|
||||
- homeassistant.event:
|
||||
event: esphome.test_event
|
||||
data:
|
||||
event_name: !lambda |-
|
||||
return event_type;
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
on_time:
|
||||
|
||||
@@ -2951,6 +2951,7 @@ def test_run_miniterm_batches_lines_with_same_timestamp(
|
||||
|
||||
mock_serial = MockSerial([chunk, MOCK_SERIAL_END])
|
||||
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32}
|
||||
config = {
|
||||
CONF_LOGGER: {
|
||||
CONF_BAUD_RATE: 115200,
|
||||
@@ -2989,6 +2990,7 @@ def test_run_miniterm_different_chunks_different_timestamps(
|
||||
|
||||
mock_serial = MockSerial([chunk1, chunk2, MOCK_SERIAL_END])
|
||||
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32}
|
||||
config = {
|
||||
CONF_LOGGER: {
|
||||
CONF_BAUD_RATE: 115200,
|
||||
@@ -3019,6 +3021,7 @@ def test_run_miniterm_handles_split_lines() -> None:
|
||||
|
||||
mock_serial = MockSerial([chunk1, chunk2, MOCK_SERIAL_END])
|
||||
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32}
|
||||
config = {
|
||||
CONF_LOGGER: {
|
||||
CONF_BAUD_RATE: 115200,
|
||||
@@ -3057,6 +3060,7 @@ def test_run_miniterm_backtrace_state_maintained() -> None:
|
||||
|
||||
mock_serial = MockSerial([backtrace_chunk, MOCK_SERIAL_END])
|
||||
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32}
|
||||
config = {
|
||||
CONF_LOGGER: {
|
||||
CONF_BAUD_RATE: 115200,
|
||||
@@ -3122,6 +3126,7 @@ def test_run_miniterm_handles_empty_reads(
|
||||
|
||||
mock_serial = MockSerial([b"", chunk, b"", MOCK_SERIAL_END])
|
||||
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32}
|
||||
config = {
|
||||
CONF_LOGGER: {
|
||||
CONF_BAUD_RATE: 115200,
|
||||
@@ -3194,6 +3199,7 @@ def test_run_miniterm_buffer_limit_prevents_unbounded_growth() -> None:
|
||||
|
||||
mock_serial = MockSerial([large_data_no_newline, final_line, MOCK_SERIAL_END])
|
||||
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP32}
|
||||
config = {
|
||||
CONF_LOGGER: {
|
||||
CONF_BAUD_RATE: 115200,
|
||||
|
||||
Reference in New Issue
Block a user