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?** **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. 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 :::tip `sequential: true` for ordered workflows
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. 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 ### 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. **Prerequisite:** Create `input_datetime.washing_machine_start` and `input_datetime.dryer_start` helpers.
@ -291,59 +291,53 @@ automation:
- platform: time - platform: time
at: "21:00:00" at: "21:00:00"
action: action:
# Step 1: Find cheapest window for washing machine - service: tibber_prices.find_cheapest_schedule
- service: tibber_prices.find_cheapest_block
data: data:
duration: "01:30:00" sequential: true
gap_minutes: 15
search_start_time: "22:00:00" search_start_time: "22:00:00"
search_end_time: "07:00:00" search_end_time: "08:00:00"
search_end_day_offset: 1 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: then:
- service: input_datetime.set_datetime - service: input_datetime.set_datetime
target: target:
entity_id: input_datetime.washing_machine_start entity_id: input_datetime.washing_machine_start
data: data:
datetime: "{{ washer_result.window.start }}" datetime: "{{ schedule.tasks[0].start }}"
- service: input_datetime.set_datetime
# Step 2: Find cheapest window for dryer AFTER washer finishes target:
# Add 15 min gap for transferring laundry entity_id: input_datetime.dryer_start
- service: tibber_prices.find_cheapest_block
data: data:
duration: "01:00:00" datetime: "{{ schedule.tasks[1].start }}"
search_start: "{{ washer_result.window.end }}" - service: notify.mobile_app
search_start_offset: "00:15:00" data:
search_end_time: "08:00:00" title: "🧺 Laundry Planned"
search_end_day_offset: 1 message: >
response_variable: dryer_result Washing: {{ schedule.tasks[0].start | as_datetime
| as_local | as_timestamp
- if: "{{ dryer_result.window_found }}" | timestamp_custom('%H:%M') }}{{ schedule.tasks[0].end
then: | as_datetime | as_local | as_timestamp
- service: input_datetime.set_datetime | timestamp_custom('%H:%M') }}
target: ({{ schedule.tasks[0].price_mean | round(1) }}
entity_id: input_datetime.dryer_start {{ schedule.price_unit }})
data: Dryer: {{ schedule.tasks[1].start | as_datetime
datetime: "{{ dryer_result.window.start }}" | as_local | as_timestamp
- service: notify.mobile_app | timestamp_custom('%H:%M') }}{{ schedule.tasks[1].end
data: | as_datetime | as_local | as_timestamp
title: "🧺 Laundry Planned" | timestamp_custom('%H:%M') }}
message: > ({{ schedule.tasks[1].price_mean | round(1) }}
Washing: {{ washer_result.window.start | as_datetime {{ schedule.price_unit }})
| as_local | as_timestamp Total: {{ schedule.total_estimated_cost | round(2) }}
| timestamp_custom('%H:%M') }}{{ washer_result.window.end {{ schedule.price_unit }}
| 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
| as_local | as_timestamp
| timestamp_custom('%H:%M') }}{{ dryer_result.window.end
| as_datetime | as_local | as_timestamp
| timestamp_custom('%H:%M') }}
({{ dryer_result.window.price_mean | round(1) }}
{{ dryer_result.price_unit }})
# Execution automations # Execution automations
- alias: "Washing Machine - Start at Planned Time" - alias: "Washing Machine - Start at Planned Time"
@ -1135,7 +1129,7 @@ automation:
| Scenario | Best approach | Why | | Scenario | Best approach | Why |
|----------|--------------|-----| |----------|--------------|-----|
| Dishwasher tonight | `find_cheapest_block` | Fixed 2h runtime, needs exact start time | | 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 | | Dishwasher + dryer (independent) | `find_cheapest_schedule` | Multiple appliances, prevent overlap |
| EV charging by morning | `find_cheapest_hours` | Flexible, can split into segments | | EV charging by morning | `find_cheapest_hours` | Flexible, can split into segments |
| Heat pump all day | Sensors (rating_level) | Continuous, adjusts every 15 min | | 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, must run uninterrupted | `find_cheapest_block` | `duration` |
| One appliance, can pause/resume | `find_cheapest_hours` | `duration`, `min_segment_duration` | | One appliance, can pause/resume | `find_cheapest_hours` | `duration`, `min_segment_duration` |
| Multiple independent appliances, no overlap | `find_cheapest_schedule` | `tasks`, `gap_minutes` | | 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` | | 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). **→ [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) - **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) - **EV charging, battery, pool pump**`find_cheapest_hours` (total runtime matters, not continuity)
- **Multiple independent appliances**`find_cheapest_schedule` (prevents overlap + manages gaps) - **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` - **"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 ### How It Works
**Default mode** (optimizes for price):
1. Tasks are sorted by duration (longest first — harder to place) 1. Tasks are sorted by duration (longest first — harder to place)
2. The longest task claims the cheapest contiguous block 2. The longest task claims the cheapest contiguous block
3. Those intervals are marked as **unavailable** 3. Those intervals are marked as **unavailable**
4. The next task finds the cheapest block in the **remaining** intervals 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) 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 ### Basic Example
<details> <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. 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 :::tip Sequential ordering
`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. 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 ### 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. 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)? :::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. **Prerequisites:** Create `input_datetime.dishwasher_start` and `input_datetime.washing_machine_start` helpers.