Addresses copilot review on #15636.
1. Fix cleanup_connection_() race with queued listener events.
While an OTA session was active, a second incoming connection would
fire esphome_socket_event_callback → esphome_wake_ota_component_any_context,
which sets pending_enable_loop_ on the (still-active) OTA component.
enable_pending_loops_() only scans the inactive section, so that flag
goes invisible. When cleanup_connection_() then called disable_loop(),
the component dropped to LOOP_DONE with a stale pending flag and
nothing to re-trigger the scan — the queued client sat forever until
some unrelated socket activity woke the main loop.
Fix: don't call disable_loop() from cleanup_connection_(). loop() has
the idempotent idle check at its top; one more dispatch after cleanup
is cheap and guarantees we re-read server_->ready() and either accept
the queued client or disable cleanly.
2. Tighten the multi-port error message. Merging is fine — the constraint
is single-port. Reworded: "Only a single port is supported for 'ota'
'platform: esphome'. Got ports [...]. Consolidate onto a single port;
configs sharing a port are merged automatically."
3. Comment drift: three call sites and the fast-select extern declaration
still referred to enable_loop_soon_any_context() and implied the hook
wakes the main loop. Updated to reflect the current mechanism (sets
pending-enable flags only; callers have already woken the main loop).
Also clarified that esphome_wake_ota_component_any_context fires on
every RCVPLUS event across all monitored sockets, so false wakes are
expected and OTA::loop() disables itself again when idle.
4. Added tests/component_tests/ota/test_esphome_ota.py covering
ota_esphome_final_validate: single instance accepted, same-port
configs merge, different-port configs rejected with cv.Invalid,
non-esphome platforms unaffected.
- uint32_to_str_(): raw pointer, internal use
- uint32_to_str(): template with compile-time buffer size check
- frac_to_str_(): raw pointer, internal use
- small_pow10(): simplify to ternary chain
Replace snprintf("%.*f") with integer-based formatting for finite
float values with accuracy_decimals 0-3 (covers virtually all sensor
usage). Falls back to snprintf for higher accuracy or NaN/Inf.
Uses lrint() with double cast for the multiply to match snprintf's
rounding behavior exactly. The fast path avoids snprintf's heavy
float formatting machinery entirely.
Also optimizes value_accuracy_with_uom_to_buf to append the UOM
string directly instead of going through snprintf.
Adds C++ unit tests that verify output matches snprintf for a range
of values including edge cases.
Benchmark: 92,961ns -> 6,484ns (14.3x faster, 2000 iterations).
Add benchmarks for format_hex_to, format_hex_pretty_to,
format_mac_addr_upper, fnv1_hash, fnv1a_hash, fnv1_hash_object_id,
parse_hex, crc8, crc16, value_accuracy_to_buf, int8_to_str, and
base64_decode to establish baseline performance data before optimization.
- TemplatableFn = delete now catches both stateful lambdas AND stateless
lambdas with inconvertible return types (e.g., string -> int)
- Add test for to_exp with non-string output_type (lambda-wraps result)