On some ESP32 boards (especially cheap clones), the eFuse custom MAC
area contains random garbage that passes the existing all-zeros/all-ones
validation. Additionally, esp_efuse_mac_get_default() can fail with CRC
errors, but the return value was being ignored, causing garbage MAC
addresses to be advertised via mDNS.
This caused Home Assistant to report false "MAC address changed" device
conflicts on every boot.
Two fixes:
- Check return values from eFuse MAC read functions and add a fallback
chain: custom MAC -> default MAC -> raw eFuse bytes -> zeroed MAC.
- Reject multicast MACs (bit 0 of first byte set) in mac_address_is_valid()
since device MACs must always be unicast.
Closes https://github.com/esphome/esphome/issues/14501
Regression tests for #15040 using a single compiled binary with two
cwww lights to verify:
- constant_brightness: true maintains constant total CW+WW power
output across all color temperatures with gamma correction
- constant_brightness: false correctly varies total power (higher
at mid-range where both channels contribute)
The gamma LUT refactor (#14123) moved gamma correction to after
the constant_brightness balancing formula (max/sum ratio). This
broke constant_brightness because gamma is nonlinear and does not
commute with the ratio calculation, causing a severe brightness
dip at mid-range color temperatures.
Fix by applying gamma to individual CW/WW/brightness values
before the constant_brightness formula, restoring the original
behavior where total power output remains constant across all
color temperatures.
Closes#15040
- Update comments to reflect that ProtoService methods moved to
APIConnection, not APIServerConnectionBase
- Fix comment referring to read_message as "override"
- Wrap #include "api_connection.h" and read_message_ implementation
in #ifdef USE_API guards
The virtual destructor was unnecessary since APIConnection is only
stored as unique_ptr<APIConnection>, never via a base class pointer.
Removing it eliminates the vtable entirely.
Rename read_message to read_message_ per clang-tidy naming convention
for protected methods.
ProtoService was an abstract interface with 6 pure virtual methods,
but APIConnection was the only concrete implementation. Move all
functionality directly into APIConnection and remove the unnecessary
virtual dispatch and vtable overhead.
check_connection_setup_() and check_authenticated_() call virtual
methods (is_connection_setup, on_no_setup_connection). When defined
in ProtoService, the compiler cannot devirtualize these calls.
Moving them to APIConnection (final) enables devirtualization.
Move read_message() from APIServerConnectionBase into APIConnection
and drop the virtual keyword from all 64 on_* handler declarations.
Since APIConnection is final and the only subclass, the virtual
dispatch was unnecessary. With read_message and the on_* handlers
in the same class, the compiler can devirtualize the calls and
inline small handlers directly into the switch cases.
Move before_loop_tasks_() to application.h as always_inline.
This removes one stack frame between the main loop and
scheduler.call(), which matters for timer callbacks that
trigger deeply nested operations.
Inline yield_with_select_ for ESP8266/RP2040 (socket_delay) and
no-socket (delay) paths in addition to the LWIP_FAST_SELECT path.
Only the select() fallback (host platform) remains in the .cpp.
This ensures yield_with_select_ is inlined into the loop on all
embedded platforms, not just ESP32/LibreTiny.
The play_complex overrides cost flash per template instantiation
across every automation. The always_inline on the forwarding chain
(Trigger::trigger, Automation::trigger, ActionList::play) is a
fixed cost that collapses 3 frames into 1.
LambdaAction and ContinuationAction overrides caused flash bloat
by replacing shared base class play_complex instantiations with
per-class copies. StatelessLambdaAction is the most common
automation action and its no-args variant is only 24 bytes.
The always_inline on Trigger::trigger(), ActionList::play(), and
Automation::trigger() duplicates code at every trigger call site,
adding ~384 bytes of flash. Revert to compiler-managed inlining
and keep only the play_complex overrides (phase 2).
When USE_LWIP_FAST_SELECT is defined (ESP32/LibreTiny), move the
yield_with_select_ implementation to application.h as always_inline.
This eliminates another stack frame from the hot loop path on these
platforms. The fallback select()/delay() paths for other platforms
remain in application.cpp.
Force-inline the Trigger→Automation→ActionList forwarding chain
and override play_complex() in leaf action classes to skip the
virtual play() dispatch, reducing the button→lambda call stack
from 8 frames to ~4.
Move Application::loop() from application.cpp to application.h as
inline ESPHOME_ALWAYS_INLINE so the compiler can inline it at all
call sites. On ESP32, loop_task() now calls App.loop() directly
instead of going through the generated loop() wrapper.
This eliminates one stack frame from the main loop call chain on
all platforms, producing cleaner crash backtraces and reducing
function call overhead on every loop iteration.
Without these flags, libsodium's sodium_memzero() falls through to the
slowest fallback: a volatile byte-by-byte zeroing loop. With them, it
uses memset() guarded by a weak-symbol call and inline asm memory
clobber, which is significantly faster.
This also improves sodium_memcmp() and sodium_compare() codegen by
allowing non-volatile pointer access with weak barriers, and enables
a timing-safe comparison optimization in crypto_verify.
Both flags are safe for all ESPHome targets: __attribute__((weak)) and
__asm__ are core GCC features supported by all toolchains (Xtensa,
RISC-V, ARM, x86_64).