enable_loop_soon_any_context() sets volatile flags but does not wake
the main loop from ulTaskNotifyTake() sleep. This means components
using ISR-driven state changes (e.g. GPIO binary sensors) wait up
to ~16ms for the select timeout before their loop runs.
Add Application::wake_loop_isrsafe(nullptr) to immediately wake the
main loop when called from ISR context on platforms with fast select.
Also relax the wake_loop_isrsafe() guard from requiring both
USE_WAKE_LOOP_THREADSAFE and USE_LWIP_FAST_SELECT to just
USE_LWIP_FAST_SELECT, since the ISR wake path uses
vTaskNotifyGiveFromISR directly and does not depend on the
UDP socket mechanism.
Use the ESP-IDF UART driver's uart_set_select_notif_callback() to wake
the main loop directly from the UART ISR via vTaskNotifyGiveFromISR(),
eliminating the FreeRTOS trampoline task and event queue that were
previously needed when wake_loop_threadsafe() used a UDP loopback socket.
With fast lwip select, the main loop sleeps on ulTaskNotifyTake(), so
the ISR-safe vTaskNotifyGiveFromISR() can wake it directly.
Saves ~2.6KB RAM per UART instance with wake-on-RX enabled:
- 2,240 bytes FreeRTOS task stack
- ~340 bytes event queue (20 entries + control block)
- 8 bytes QueueHandle_t + TaskHandle_t fields
Also adds Application::wake_loop_isrsafe() as the ISR-safe counterpart
to wake_loop_threadsafe(), backed by esphome_lwip_wake_main_loop_from_isr().
The two_byte_to_int() function used `char` parameters instead of
`uint8_t`, causing sign extension for byte values >= 0x80. This
produced incorrect sensor values for distances where value % 256 >= 128
(e.g. 128cm reported as -128, 200cm reported as -56).
Changed return type to uint16_t to match protocol spec (unsigned
distances), renamed to two_byte_to_uint16, and moved to shared
ld24xx header to eliminate duplication.
The two_byte_to_int() function used `char` parameters instead of
`uint8_t`, causing sign extension for byte values >= 0x80. This
produced incorrect sensor values for distances where value % 256 >= 128
(e.g. 128cm reported as -128, 200cm reported as -56).
Also removed the unnecessary `(int16_t)` cast and switched from `+` to
`|` for proper bitwise assembly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- thermostat: Use CLIMATE_FAN_ON to match other callsites in the file
- sprinkler: Use *opt instead of .value() for consistency with line 47
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add this-> to fan_mode in haier hon_climate and smartair2_climate
- Add this-> to voc/nox_tuning_params_ in sgp4x
- Use init-statement for max_refresh_rate_ in esp32_rmt_led_strip
- Use init-statement for fan_mode/preset in climate_ir
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract the keepalive ping/disconnect cold path into a separate
noinline check_keepalive_() method. This code only fires once per
minute but was contributing ~110 bytes to the hot loop() body that
runs every ~16ms.
Results on ESP32 (xtensa):
- loop(): 374 → 263 bytes (-111 bytes, -30%)
- check_keepalive_(): 136 bytes (new, cold path)
- Net: +25 bytes total (call overhead), but the hot path icache
footprint is significantly reduced.
The template wrapper caused the compiler to make different
optimization decisions on setup(), negating the savings.
Calling register_component_ directly was -4 bytes.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Using the original register_component name (just moved to protected)
produces identical compiled output to the baseline, avoiding the
symbol name length overhead from renaming.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Rename the non-template to register_component_impl_ and add
a protected template register_component_ that wraps it. This
preserves the compiler optimization behavior (isra clones)
while keeping the method inaccessible to external components.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Without the template wrapper, the compiler inlines the 24-byte
function at each of ~92 call sites in setup(), growing setup()
by ~284 bytes. Force a function call instead.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Arduino.h on ESP8266 already declares void setup(void), so our
forward declaration triggers readability-redundant-declaration.
Co-Authored-By: J. Nick Koston <nick@koston.org>
The cpp test framework renames setup() to original_setup() and
replaces setup() with the gtest runner, so we need to friend both.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Instead of runtime null, duplicate, and capacity checks in
register_component_, make the method unreachable from outside
codegen by removing the public template wrapper and granting
friend access to the codegen-generated ::setup() function.
Since ESPHOME_COMPONENT_COUNT is set to exactly
len(CORE.component_ids) at codegen time, the StaticVector is
always correctly sized and the runtime capacity check cannot
trigger from codegen. External components that bypassed codegen
to call App.register_component() directly will now get a
compile error, forcing them to properly declare their components
in their config schema.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Fix clang-tidy bugprone-unchecked-optional-access in speed fan,
speaker media player, and sprinkler components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Store optional results in local variables before checking
and dereferencing to satisfy bugprone-unchecked-optional-access.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move set_component_state_ from component.cpp to the header as
an inline method so it can be reused wherever Component state
needs to be updated. Replace the manual bit manipulation in
Application::enable_pending_loops_ with a call to the helper.
Co-Authored-By: J. Nick Koston <nick@koston.org>
get_state() requires callers to check has_state() first.
This is a documented precondition, not an unchecked access.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The custom optional implementation (from optional-bare, 2017) predates
C++17. All ESPHome platforms now compile with gnu++20, making
std::optional available everywhere.
The custom implementation had several issues:
- No emplace() support
- Always default-constructs value_ (wasteful for non-trivial types)
- reset() only flips a bool without destroying the value
- No move semantics
- Requires T to be default constructible
Replace with using aliases (using std::optional, using std::nullopt,
etc.) so all existing code using esphome::optional continues to work.
Also fix ~30 unsafe .value() calls across climate IR components that
relied on the custom optional's behavior of returning a
default-constructed value when empty. With std::optional, accessing
an empty optional is UB. These are replaced with value_or() using
appropriate defaults (CLIMATE_FAN_AUTO, CLIMATE_PRESET_NONE).