- 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
Replace 4-byte component_source_ pointer with a 9-bit index into a
PROGMEM lookup table generated by Python codegen. Source names are
deduplicated at codegen time (same pattern as EntityStringPool).
Shrink warn_if_blocking_over_ from uint16_t (ms) to uint8_t
(centiseconds), saturating at 255 (2550ms).
Saves 4 bytes per component instance (typical configs: 120-200 bytes).
Scope the deprecation warning suppression to just the constructor
and publish_state() instead of the entire file, so unrelated
deprecation warnings aren't accidentally hidden.
Avoids requiring T to be default-constructible, which would break
future instantiations with non-default-constructible state types.
Capture old_state before set_state_value() since the pointer aliases
subclass storage that gets overwritten.
Check state == new_state directly before calling virtual set_new_state.
Avoids the virtual dispatch chain (BinarySensor::set_new_state →
StatefulEntityBase::set_new_state → virtual get_state()) on the
common no-change path. Fixes -20% CodSpeed regression on
BinarySensorPublish_NoChange benchmark.
- Cache get_state() result to avoid calling the virtual method twice
(once for comparison, once for old_state construction)
- Move full_state_callbacks_.call() inside the !empty() guard so both
the optional construction and the call are skipped when no callbacks
- Swap to had_state || get_trigger_on_initial_state() so the virtual
call is skipped on the common path (every change except the first)
Saves ~21 bytes of flash in set_new_state.
When WiFi disconnects while a roaming scan is in progress, the
SCANNING state was transitioning to IDLE in retry_connect(). When the
device then reconnected, check_connecting_finished() treated the IDLE
state as a normal connection and reset roaming_attempts_ to 0. This
caused the roaming counter to perpetually restart at 1/3 instead of
progressing to 2/3, 3/3, creating an infinite loop of roaming scans
every 5 minutes.
This is most likely to occur on ESP8266 where disconnect callbacks
run immediately in SDK system context (setting error_from_callback_
before the main loop can process scan results), compared to ESP32 IDF
where events are queued and the scan-done event is typically processed
before the disconnect.
Fix by transitioning to RECONNECTING instead of IDLE when a disconnect
occurs during SCANNING. This preserves the attempts counter on
successful reconnection, allowing the device to reach the 3-attempt
limit and stop scanning.
Same pattern as sensor: raw_callback_ is only needed when filters are
configured. add_on_raw_state_callback remains always available —
without filters it delegates to callback_ since raw == filtered.
Saves 4 bytes per TextSensor instance on builds without filters.
When Pvariable types contain template arguments with :: (e.g.
Automation<std::optional<bool>>), the namespace extraction logic
incorrectly split on :: inside the template params, producing
invalid C++ identifiers like Automation<std__automation_id__pstorage.
Fix by stripping template arguments before extracting the namespace.
Extract the logic into a testable _extract_component_ns helper.
add_on_raw_state_callback is always available now (delegates to
callback_ without filters). SensorRawStateTrigger doesn't need
the guard, and on_raw_value doesn't need to set USE_SENSOR_FILTER.
Without filters raw == filtered, so the trigger fires with the
correct value either way — no wasted RAM on filter infrastructure.
Delegate to callback_ when USE_SENSOR_FILTER is off since raw state
equals filtered state without filters. This avoids requiring external
components to know about USE_SENSOR_FILTER.
set_trigger_on_initial_state is only called from codegen on concrete
BinarySensor instances. It's an implementation detail of BinarySensor,
not part of the StatefulEntityBase contract. Only get_trigger_on_initial_state
remains as a pure virtual — subclasses decide how to control it.