Commit Graph
340 Commits
Author SHA1 Message Date
J. Nick Koston 20743e3e9a Merge branch 'scheduler-pool-unbounded-freelist' into integration 2026-04-30 11:06:46 -05:00
J. Nick Koston f9b87d0ede [core] Replace scheduler pool vector with unbounded intrusive freelist
The fixed MAX_POOL_SIZE=5 cap was the source of the heap churn the pool was
meant to prevent: any device with more than 5 concurrent timers (e.g. a board
with 30+ LD2450 sensors) hit a steady-state oscillation of recycle->delete and
acquire->new on every loop iteration.

Replace std::vector<SchedulerItem*> with a singly-linked freelist threaded
through SchedulerItem::next_free, which shares storage with `component` via an
anonymous union (zero per-item overhead -- the component pointer is dead while
pooled). Drop the cap entirely: the freelist quiesces at the application's
natural concurrent-timer high-water mark, which is the working set the device
already needs while those timers are active.

No std::vector means no growth-doubling slack and no realloc copies during
warm-up. Caller of get_item_from_pool_locked_() must overwrite item->component
before unlocking (already true at the sole call site); nullptr remains a valid
live `component` value for SELF_POINTER items, so we cannot pre-clear it.
2026-04-30 10:42:46 -05:00
J. Nick Koston 8246b09445 Merge branch 'ota-watchdogmanager' into integration 2026-04-29 14:34:36 -05:00
J. Nick Koston c41f38e16d [scheduler] Add self-keyed timer API for callers without a Component (#16127) 2026-04-29 13:24:37 -05:00
J. Nick Koston e5b1991cf7 [fan] Add tests for fan.turn_on action field combinations (#16125) 2026-04-29 12:46:06 -05:00
J. Nick Koston 7fba57ce51 [valve] Add tests for valve.control action field combinations (#16126) 2026-04-29 12:45:30 -05:00
J. Nick Koston ba34021e8e Merge remote-tracking branch 'upstream/sensor-filters-drop-component' into integration 2026-04-29 10:21:49 -05:00
J. Nick Koston 9745a3299e [scheduler] Address review feedback (const void *, %p cast, buffer size) 2026-04-29 09:34:45 -05:00
J. Nick Koston 241d7797e3 [scheduler] Add self-keyed timer API for callers without a Component 2026-04-29 09:18:09 -05:00
J. Nick Koston dc48f2df13 Merge remote-tracking branch 'origin/cover-action-bitmask' into integration 2026-04-29 07:41:16 -05:00
J. Nick Koston 01a4769a5d Merge remote-tracking branch 'origin/climate-control-action-bitmask' into integration 2026-04-29 07:41:08 -05:00
J. Nick Koston f7c8df8234 [cover] Address review feedback (const ref args, drop mask numbering) 2026-04-29 06:56:18 -05:00
J. Nick Koston a7a45a556b [climate] Address review feedback (const ref args, utf-8 byte length, comments) 2026-04-29 06:54:23 -05:00
J. Nick Koston 6f79312fa7 Merge branch 'dev' into cover-action-bitmask 2026-04-29 05:42:09 -05:00
J. Nick Koston 1ea7d20d98 Merge branch 'dev' into climate-control-action-bitmask 2026-04-29 05:42:01 -05:00
J. Nick Koston 5a33c50015 [light] Use constexpr template for DimRelativeAction transition_length (#16038) 2026-04-29 14:26:38 +12:00
J. Nick Koston 0d150dc57e [light] Use constexpr template for ToggleAction transition_length (#16037) 2026-04-29 14:25:18 +12:00
J. Nick Koston f72173550d Merge remote-tracking branch 'origin/webserver-list-entities-xmacro' into integration 2026-04-28 10:39:10 -05:00
Bonne EgglestonandJ. Nick Koston 52e8c50f45 [modbus] Split modbus_server from modbus_controller (#15509)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-04-28 10:21:25 -05:00
J. Nick Koston d26e29d89c Merge remote-tracking branch 'origin/light-toggle-action-constexpr-template' into integration 2026-04-27 06:46:06 -05:00
J. Nick Koston 4ed7956094 Merge remote-tracking branch 'origin/light-dim-relative-constexpr-template' into integration 2026-04-27 06:46:00 -05:00
J. Nick Koston 1135eeaf99 Merge remote-tracking branch 'origin/cover-action-bitmask' into integration 2026-04-27 06:45:48 -05:00
J. Nick Koston 9089cc968e [climate] Use heat_cool_mode (not auto_mode) to enable HEAT_COOL in test fixture 2026-04-27 05:42:18 -05:00
J. Nick Koston 6770a6b87f [cover] Address Copilot review on integration test fixture and macro comments 2026-04-27 05:39:21 -05:00
J. Nick Koston fdb183f772 [climate] Add auto_mode to thermostat fixture so HEAT_COOL is supported 2026-04-27 05:21:27 -05:00
J. Nick Koston 2eac9f5121 [light] Add integration test for DimRelativeAction 2026-04-27 05:05:00 -05:00
J. Nick Koston e9ced485ea [light] Add integration test for ToggleAction 2026-04-27 05:04:28 -05:00
J. Nick Koston 66115c117a [climate] Add integration test for ControlAction 2026-04-27 05:03:57 -05:00
J. Nick Koston 3522eef8ee [cover] Add integration test for ControlAction/CoverPublishAction 2026-04-27 05:02:58 -05:00
J. Nick Koston e4f413adad [core] decouple main loop cadence from scheduler wake timing (#15792) 2026-04-21 14:48:21 +02:00
J. Nick Koston 574bdc9a00 Merge remote-tracking branch 'origin/decouple_scheduler_loop_cadence' into integration 2026-04-20 12:06:28 +02:00
J. Nick Koston 78aabf257f [core] wake_loop_threadsafe() forces a component-phase iteration
After the Phase A / Phase B split in this PR, an external producer that
called wake_loop_threadsafe() (MQTT RX, USB RX, BLE event, espnow,
camera, mWW, speakers, USB host/CDC, lwip socket, enable_loop_soon_any_context)
only got Phase A — the component phase stayed gated by loop_interval_,
so the producer's component loop() could be delayed by up to
loop_interval_ ms before draining its queued work. That breaks the
long-standing semantic of wake_loop_threadsafe().

Add a wake_request flag set by every wake_loop_* entry point and
exchange-cleared at the gate in Application::loop(). When the flag is
set, force Phase B regardless of loop_interval_.

Storage is conditional on the threading model:
  - ESPHOME_THREAD_MULTI_ATOMICS: std::atomic<uint8_t> (uint8_t, not
    bool, because GCC on Xtensa generates an indirect call for
    atomic<bool> ops — same workaround as scheduler.h)
  - ESPHOME_THREAD_SINGLE / ESPHOME_THREAD_MULTI_NO_ATOMICS: volatile
    uint8_t (8-bit aligned loads/stores are atomic on every supported
    MCU; the platform signal that follows wake_request_set provides the
    cross-thread/cross-core memory barrier)

Helpers (wake_request_set / wake_request_take) are always_inline so
IRAM_ATTR call sites stay in IRAM. Set BEFORE the platform signal so the
consumer is guaranteed to see the flag on its next gate check.

Adds an integration test that raises loop_interval_ to 2s, snapshots a
counting component's loop count, spawns a std::thread that calls
App.wake_loop_threadsafe() after 50ms, and asserts the count increments
inside a 500ms observation window. Without the fix the count would not
move for ~2s.
2026-04-20 11:52:56 +02:00
J. Nick Koston a036a9eab3 Merge branch 'decouple_scheduler_loop_cadence' into integration 2026-04-18 17:33:39 -05:00
J. Nick Koston 7d12b984a8 [core] address review polish on main-loop decoupling
Doc and test updates from a code review of this PR:

- Correct the `tail_us == 0 on Phase A-only ticks` claim in the
  Application::loop() comment and the RuntimeStatsCollector::record_loop_active
  docstring. `loop_tail_start_us` is set to `loop_before_end_us`, and
  `loop_now_us` is sampled later, so `tail_us` on Phase A-only ticks is
  the small gate-check + record prefix — tiny but non-zero.
  (Also flagged by Copilot on application.h:623 and runtime_stats.h:45.)

- Call out ESP8266 as the floor case in the WDT_FEED_INTERVAL_MS margin
  table. Its soft WDT (~1.6 s) is the tightest margin at ~5x, so future
  changes to the constant need to preserve comfortable headroom there.

- Tighten the test lower bound at tests/integration/test_loop_interval_decoupling.py
  from `2 <= loop_delta <= 6` to `3 <= loop_delta <= 6`. Allowing 2 would
  let a >50% slowdown from the 4-in-2s nominal pass as CI jitter, which
  undermines the regression signal. 3 keeps the test honest while still
  absorbing realistic CI jitter.

- Add a second integration test
  (test_loop_interval_default_not_pulled_forward) that covers the inverse
  direction: at the default loop_interval_ with a fast scheduler item
  (5 ms — well under the old delay_time/2 = 8 ms floor), the component
  phase must still run at ~62 Hz, not the pre-fix ~128 Hz. This locks
  down the original 128 Hz → 62 Hz regression that motivated the PR.
2026-04-18 17:32:29 -05:00
J. Nick Koston 6abb9fd815 Merge remote-tracking branch 'upstream/decouple_scheduler_loop_cadence' into integration
# Conflicts:
#	esphome/core/application.h
2026-04-18 07:12:36 -05:00
J. Nick Koston 43d98cb445 Merge remote-tracking branch 'upstream/dev' into decouple_scheduler_loop_cadence
# Conflicts:
#	esphome/core/scheduler.cpp
2026-04-17 09:33:59 -05:00
J. Nick Koston 162bd415ce Merge remote-tracking branch 'upstream/fix-delay-action-mutable-lambda' into integration 2026-04-17 08:36:08 -05:00
J. Nick Koston 523c6f2376 [core] coerce set_interval(0) / update_interval: 0ms to 1ms (#15799) 2026-04-17 02:45:50 -10:00
J. Nick Koston a0ac226e6c [core] decouple main loop cadence from scheduler wake timing
Restructures Application::loop() into two independent phases to stop the
scheduler from silently pulling the component loop cadence forward.

Before: Application::loop() bounded its sleep by
  min(loop_interval_ - elapsed, next_schedule_in())
with a delay_time/2 floor. Any scheduler item due sooner than
loop_interval_/2 dragged the whole component phase with it. On a typical
ESP32 config with default loop_interval_=16ms, combined scheduler
activity from api / esp32_ble / esp32_ble_tracker / debug was keeping
every component's loop() running at ~128 Hz instead of the documented
~62 Hz.

This has become more visible recently as more components convert to
PollingComponent (which uses set_interval internally) and more in-tree
code uses set_interval / set_timeout directly. Adding or removing any
scheduled item silently changed every other component's loop cadence.
App.set_loop_interval() for power savings was also silently defeated.

After:
- Phase A (every tick): drain wake notifications, run scheduler.call(),
  feed WDT
- Phase B (gated by loop_interval_ or HighFrequencyLoopRequester):
  iterate registered components and update last_loop_

Sleep = min(time-until-next-component-phase, next_schedule_in()). When
a scheduler event wakes us early, Phase A services it and the component
phase stays gated independently. loop_interval_ is now a true minimum
interval between component phases.

The delay_time/2 floor is removed. Any legitimate need to wake faster
than loop_interval_ has proper mechanisms:
- HighFrequencyLoopRequester for sustained fast-loop needs
- Application::wake_loop_threadsafe() from any context (new in 2026.4.0)
  for one-shot wake-on-event

Also guards against set_interval(0) misuse — it asks the main loop to
spin forever, which was never the intended API. Warns at creation time
pointing authors at HighFrequencyLoopRequester. set_timeout(0)/defer()
is unaffected; zero-delay one-shots remain legitimate.

Runtime stats: process_pending_stats is now called on every tick (not
just when the component phase runs) so log_interval_ isn't quantized to
the component-phase cadence. Added an inline fast-path gate in
runtime_stats.h that early-outs unless now >= next_log_time_, keeping
Application::loop() slim; the log_stats_ work stays out-of-line.

Ordering constraints preserved:
- defer() callbacks still FIFO before components same-tick (Phase A
  runs before Phase B)
- Scheduled items still execute before components when both due
- Scheduled callbacks still run on main thread only
- loop_component_start_time_ is still set fresh at each component's loop
- WDT is still fed at least once per tick
2026-04-16 14:27:39 -10:00
J. Nick Koston e48c7165c5 [light] Avoid addressable transition stall at low gamma-corrected values (#15726) 2026-04-15 07:45:42 +12:00
J. Nick Koston da9fbb8044 [core] Fix app_state_ status bits clobbered for non-looping components (#15658) 2026-04-14 07:50:11 -10:00
J. Nick Koston 4696d70b8a Merge remote-tracking branch 'origin/light-addressable-gamma-transition-stall' into integration 2026-04-13 15:04:07 -10:00
J. Nick Koston b324630f8e [light] Type-annotate to_code in mock_addressable_light 2026-04-13 14:56:02 -10:00
J. Nick Koston 32130e1cb1 [light] Address Copilot review feedback on PR #15726
- mock_addressable_light.h: add direct <memory>/<cstdint>/<cstddef> includes
- test: use asyncio.get_running_loop() instead of deprecated get_event_loop()
- test: rebase timing to command-issue time (not first-nonzero) and use
  absolute progress for assertion 2, so late-transition check can't skew
  when the first nonzero sample happens to land near the assertion-1 limit
2026-04-13 14:53:17 -10:00
J. Nick Koston 3f56e0255a [light] Avoid addressable transition stall at low gamma-corrected values
When a uniform-colored addressable strip transitions from one color to
another, interpolate math-only against a cached start color instead of
reading each LED's current value back through the 8-bit stored byte.

The old algorithm used led.get_red()/etc. every step as the source for
the delta, which round-tripped through gamma uncorrect/correct and the
8-bit stored byte. At gamma 2.8, any pre-gamma value below ~27 rounds
to stored byte 0, so small early-transition steps produced stored 0 and
the next step read back 0, stalling progress until ~90% of the transition
before a single step produced a large-enough pre-gamma value to clear
the gamma threshold. Result: dark for the first 9s of a 10s fade, then
jump on in the final 1s.

Detect uniform start state in start() and take a cheap math-only lerp
path when true, so the stored byte advances through each gamma threshold
as smoothed_progress crosses it. Falls back to the existing per-LED
read-back algorithm when the buffer is non-uniform (e.g. when
transitioning out of an addressable effect).
2026-04-13 14:17:21 -10:00
J. Nick Koston 8800c10181 [core] Address review: fix docstrings per Copilot feedback
- get_app_state(): say 'STATUS_LED_* only', not 'STATUS_LED_* and lifecycle'
  since lifecycle bits are no longer maintained in app_state_.
- STATUS_LED_SETTLE_S: remove incorrect mention of feed_wdt re-dispatch;
  status_led_light is driven by the main loop, not feed_wdt.
- snapshot_led service: say 'status_led_light output' not 'pin state'
  since the fixture uses a template output, not GPIO.
2026-04-11 17:37:59 -10:00
J. Nick Koston 79f27d7a19 Merge branch 'drop-redundant-app-state-or' into integration 2026-04-11 16:39:14 -10:00
J. Nick Koston 424f1c7b0a [core] Rename any_component_has_status_flag and add integration tests
Drop the trailing underscore from any_component_has_status_flag now
that the method is public. Trailing underscores in the codebase are
reserved for protected/private members per clang-tidy naming rules,
which caused a CI failure on the previously-named public helper.

Add integration tests covering:
- Single-component status_set/clear for warning and error
- Multi-component OR semantics (both clear orders)
- Warning and error independence
- End-to-end proof that status_led_light::loop() reads App.app_state_
  and writes its output when the bits are set (via a fake template
  output whose write_action bumps a counter exposed as a sensor)
2026-04-11 16:20:05 -10:00
J. Nick Koston 8e02d0a20e [fan] Store preset mode vector on Fan entity to eliminate heap allocation (#15209) 2026-04-09 10:25:37 +12:00
J. Nick Koston faa05031a7 [climate] Store custom mode vectors on Climate entity to eliminate heap allocation (#15206) 2026-04-09 10:25:29 +12:00