Use 100-300ms dwell times for roaming scans instead of 400-500ms,
matching the ESP32 IDF scan times. This reduces total off-channel
time from ~6.5s to ~3.9s (13 channels), which reduces the chance
of triggering Beacon Timeout disconnects on aggressive APs.
The longer dwell times are preserved for initial connection scans
where a thorough survey is needed.
On ESP8266, the roaming scan goes off-channel for ~3 seconds. Some
APs aggressively disconnect the client (Beacon Timeout) 8-20 seconds
after the scan finishes. By that time, process_roaming_scan_() has
already set roaming_state_ back to IDLE. When retry_connect() runs,
it sees IDLE and calls clear_roaming_state_(), resetting the attempts
counter to 0. This creates an infinite loop of "attempt 1/3" every
5 minutes.
The aggressive AP behavior cannot be fixed from the ESP side, but we
can prevent the counter from resetting so roaming eventually stops
after 3 attempts.
Fix by recording when the roaming scan completes and adding a 30s
grace period. If a disconnect occurs within that window, the state
transitions to RECONNECTING (preserving the counter) instead of
clearing it.
Closes https://github.com/esphome/esphome/issues/15124
Remove last_state_ from GPIOBinarySensorStore — it was always set to
the same value as state_ in the ISR and never written elsewhere, making
the comparison `new_state != last_state_` equivalent to
`new_state != state_`.
Move use_interrupt_ and interrupt_type_ into the store to fill the
padding freed by removing last_state_, eliminating all internal padding.
Store layout (12B, zero padding):
isr_pin_.arg_* 4B
component_* 4B
state_ 1B (volatile)
changed_ 1B (volatile)
use_interrupt_ 1B
interrupt_type_ 1B
Saves 4 bytes per GPIOBinarySensor instance (64B → 60B).
Move restore_mode_ (uint8_t) next to next_write_ and
is_transformer_active_ (both bool) so all three pack into
a single 4-byte slot. Previously restore_mode_ sat alone
at the end of the struct with 3 bytes of trailing padding.
Saves 4 bytes per LightState instance.
Save 16 bytes per GPIOSwitch instance (FixedVector<Switch*> 12B +
uint32_t 4B) by guarding interlock fields and logic behind
USE_GPIO_SWITCH_INTERLOCK, which is only defined when the interlock
config option is present.
- Test empty name returns 0
- Test deduplication returns same index
- Test overflow warns and returns 0
- Remove duplicate index==0 check in get_component_log_str() since
component_source_lookup() already handles it
- Drop 9-bit index scheme in favor of plain uint8_t (max 255 unique
source names, overflow warns in Python and returns 0)
- Add friend declarations for setup()/original_setup() so generated
code can call protected set_component_source_()
- Move get_component_log_str() out of line
- Fix test mocks to provide CORE.data dict