docs(scheduling): update docs for sequential scheduling parameter

Replace workaround recommendations (2× find_cheapest_block) with the
new sequential: true parameter. Rewrite washer→dryer example as a single
find_cheapest_schedule call. Update quick reference tables.

Release-Notes: skip
This commit is contained in:
Julian Pawlowski 2026-04-19 14:17:41 +00:00
parent 31fca73ccd
commit a8d1519a26
2 changed files with 55 additions and 52 deletions

View file

@ -270,13 +270,13 @@ automation:
**Why `find_cheapest_schedule` instead of separate `find_cheapest_block` calls?**
If you call `find_cheapest_block` separately for each appliance, they might both pick the **same** cheap window. `find_cheapest_schedule` reserves each slot exclusively — the dryer gets the next-cheapest window after the dishwasher claims its slot, with a 15-minute gap between them.
:::caution `find_cheapest_schedule` does NOT guarantee order
The scheduler optimizes purely for price — the dryer might be scheduled **before** the dishwasher if that results in lower total cost. This is fine for independent appliances, but problematic for sequential workflows (e.g., washing machine → dryer). See the next example for that.
:::tip `sequential: true` for ordered workflows
By default, `find_cheapest_schedule` optimizes purely for price — the dryer might be scheduled **before** the dishwasher. This is fine for independent appliances. For sequential workflows (e.g., washing machine → dryer), add `sequential: true` — see the next example.
:::
### Washing Machine → Dryer: Sequential Scheduling
When the dryer **must** run after the washing machine, you need guaranteed order. Since `find_cheapest_schedule` doesn't guarantee task ordering, use **two sequential `find_cheapest_block` calls** instead:
When the dryer **must** run after the washing machine, use `sequential: true` to guarantee declaration-order scheduling. The scheduler places each task after the previous one finishes (plus gap).
**Prerequisite:** Create `input_datetime.washing_machine_start` and `input_datetime.dryer_start` helpers.
@ -291,59 +291,53 @@ automation:
- platform: time
at: "21:00:00"
action:
# Step 1: Find cheapest window for washing machine
- service: tibber_prices.find_cheapest_block
- service: tibber_prices.find_cheapest_schedule
data:
duration: "01:30:00"
sequential: true
gap_minutes: 15
search_start_time: "22:00:00"
search_end_time: "07:00:00"
search_end_time: "08:00:00"
search_end_day_offset: 1
response_variable: washer_result
tasks:
# Order matters! Washer runs first, dryer after.
- name: washing_machine
duration: "01:30:00"
- name: dryer
duration: "01:00:00"
response_variable: schedule
- if: "{{ washer_result.window_found }}"
- if: "{{ schedule.all_tasks_scheduled }}"
then:
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.washing_machine_start
data:
datetime: "{{ washer_result.window.start }}"
# Step 2: Find cheapest window for dryer AFTER washer finishes
# Add 15 min gap for transferring laundry
- service: tibber_prices.find_cheapest_block
data:
duration: "01:00:00"
search_start: "{{ washer_result.window.end }}"
search_start_offset: "00:15:00"
search_end_time: "08:00:00"
search_end_day_offset: 1
response_variable: dryer_result
- if: "{{ dryer_result.window_found }}"
then:
datetime: "{{ schedule.tasks[0].start }}"
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.dryer_start
data:
datetime: "{{ dryer_result.window.start }}"
datetime: "{{ schedule.tasks[1].start }}"
- service: notify.mobile_app
data:
title: "🧺 Laundry Planned"
message: >
Washing: {{ washer_result.window.start | as_datetime
Washing: {{ schedule.tasks[0].start | as_datetime
| as_local | as_timestamp
| timestamp_custom('%H:%M') }}{{ washer_result.window.end
| timestamp_custom('%H:%M') }}{{ schedule.tasks[0].end
| as_datetime | as_local | as_timestamp
| timestamp_custom('%H:%M') }}
({{ washer_result.window.price_mean | round(1) }}
{{ washer_result.price_unit }})
Dryer: {{ dryer_result.window.start | as_datetime
({{ schedule.tasks[0].price_mean | round(1) }}
{{ schedule.price_unit }})
Dryer: {{ schedule.tasks[1].start | as_datetime
| as_local | as_timestamp
| timestamp_custom('%H:%M') }}{{ dryer_result.window.end
| timestamp_custom('%H:%M') }}{{ schedule.tasks[1].end
| as_datetime | as_local | as_timestamp
| timestamp_custom('%H:%M') }}
({{ dryer_result.window.price_mean | round(1) }}
{{ dryer_result.price_unit }})
({{ schedule.tasks[1].price_mean | round(1) }}
{{ schedule.price_unit }})
Total: {{ schedule.total_estimated_cost | round(2) }}
{{ schedule.price_unit }}
# Execution automations
- alias: "Washing Machine - Start at Planned Time"
@ -1135,7 +1129,7 @@ automation:
| Scenario | Best approach | Why |
|----------|--------------|-----|
| Dishwasher tonight | `find_cheapest_block` | Fixed 2h runtime, needs exact start time |
| Washer → dryer (must be sequential) | 2× `find_cheapest_block` | Second call uses first result's end time as start |
| Washer → dryer (must be sequential) | `find_cheapest_schedule` | `sequential: true` + `gap_minutes` for guaranteed order |
| Dishwasher + dryer (independent) | `find_cheapest_schedule` | Multiple appliances, prevent overlap |
| EV charging by morning | `find_cheapest_hours` | Flexible, can split into segments |
| Heat pump all day | Sensors (rating_level) | Continuous, adjusts every 15 min |
@ -1164,7 +1158,7 @@ automation:
| One appliance, must run uninterrupted | `find_cheapest_block` | `duration` |
| One appliance, can pause/resume | `find_cheapest_hours` | `duration`, `min_segment_duration` |
| Multiple independent appliances, no overlap | `find_cheapest_schedule` | `tasks`, `gap_minutes` |
| Sequential chain (A must finish before B) | 2× `find_cheapest_block` | Use A's end as B's `search_start` |
| Sequential chain (A must finish before B) | `find_cheapest_schedule` | `sequential: true`, `gap_minutes` |
| Find the worst time (avoid it) | `find_most_expensive_block` | `duration` |
**→ [Scheduling Actions — Full Guide](scheduling-actions.md)** for all parameters, response formats, and advanced options (power profiles, relaxation, outlier smoothing).

View file

@ -33,7 +33,7 @@ flowchart TD
- **Dishwasher, washing machine, dryer**`find_cheapest_block` (must run X hours straight)
- **EV charging, battery, pool pump**`find_cheapest_hours` (total runtime matters, not continuity)
- **Multiple independent appliances**`find_cheapest_schedule` (prevents overlap + manages gaps)
- **Sequential chain (A must finish before B)**2× `find_cheapest_block` (use A's end as B's search start)
- **Sequential chain (A must finish before B)**`find_cheapest_schedule` with `sequential: true` (guaranteed order + gap)
- **"When should I NOT run this?"** → `find_most_expensive_block` or `find_most_expensive_hours`
---
@ -598,12 +598,21 @@ Schedules **multiple appliances** within the same search range, ensuring they do
### How It Works
**Default mode** (optimizes for price):
1. Tasks are sorted by duration (longest first — harder to place)
2. The longest task claims the cheapest contiguous block
3. Those intervals are marked as **unavailable**
4. The next task finds the cheapest block in the **remaining** intervals
5. Optional gap between tasks ensures a pause (e.g., for shared plumbing or circuit recovery)
**Sequential mode** (`sequential: true` — guarantees order):
1. Tasks are placed in **declaration order** (the order you list them)
2. Each task's search window starts after the previous task ends (+ gap)
3. Price optimization still applies **within** each task's available window
4. If a task can't be placed, all subsequent tasks are also unscheduled (the chain breaks)
### Basic Example
<details>
@ -730,8 +739,8 @@ response_variable: result
If you call `find_cheapest_block` separately for each appliance, they might all find the **same** cheap time window. `find_cheapest_schedule` solves this by tracking which intervals are already claimed — each appliance gets its own non-overlapping slot.
:::caution No ordering guarantee
`find_cheapest_schedule` optimizes purely for **price** — it does not guarantee task order. The dryer could be scheduled before the washing machine if that's cheaper. For sequential workflows (washing machine → dryer), use **two sequential `find_cheapest_block` calls** where the second call starts after the first result ends. See [Automation Examples — Sequential Scheduling](automation-examples.md#washing-machine--dryer-sequential-scheduling) for a complete example.
:::tip Sequential ordering
By default, `find_cheapest_schedule` optimizes purely for **price** — it does not guarantee task order. The dryer could be scheduled before the washing machine if that's cheaper. For sequential workflows (washing machine → dryer), add `sequential: true` to guarantee declaration-order scheduling. See [Automation Examples — Sequential Scheduling](automation-examples.md#washing-machine--dryer-sequential-scheduling) for a complete example.
:::
### Gap Minutes
@ -815,7 +824,7 @@ All examples below use `input_datetime` helpers to store planned start times. Th
Schedule dishwasher + washing machine to run overnight at cheapest prices, with a 15-minute gap between them. These appliances are **independent** — either can run first.
:::tip Sequential appliances (e.g., washer → dryer)?
If one appliance **must** finish before another starts, don't use `find_cheapest_schedule` — it doesn't guarantee order. Use **two sequential `find_cheapest_block` calls** instead. See [Automation Examples — Sequential Scheduling](automation-examples.md#washing-machine--dryer-sequential-scheduling).
If one appliance **must** finish before another starts, add `sequential: true` to your `find_cheapest_schedule` call — this guarantees tasks run in the order you list them. See [Automation Examples — Sequential Scheduling](automation-examples.md#washing-machine--dryer-sequential-scheduling).
:::
**Prerequisites:** Create `input_datetime.dishwasher_start` and `input_datetime.washing_machine_start` helpers.