diff --git a/docs/user/docs/automation-examples.md b/docs/user/docs/automation-examples.md index e695d61..1b3c65f 100644 --- a/docs/user/docs/automation-examples.md +++ b/docs/user/docs/automation-examples.md @@ -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 + datetime: "{{ schedule.tasks[0].start }}" + - service: input_datetime.set_datetime + target: + entity_id: input_datetime.dryer_start 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: - - service: input_datetime.set_datetime - target: - entity_id: input_datetime.dryer_start - data: - datetime: "{{ dryer_result.window.start }}" - - service: notify.mobile_app - data: - title: "🧺 Laundry Planned" - message: > - Washing: {{ washer_result.window.start | as_datetime - | as_local | as_timestamp - | timestamp_custom('%H:%M') }}–{{ washer_result.window.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 - | 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 }}) + datetime: "{{ schedule.tasks[1].start }}" + - service: notify.mobile_app + data: + title: "🧺 Laundry Planned" + message: > + Washing: {{ schedule.tasks[0].start | as_datetime + | as_local | as_timestamp + | timestamp_custom('%H:%M') }}–{{ schedule.tasks[0].end + | as_datetime | as_local | as_timestamp + | timestamp_custom('%H:%M') }} + ({{ 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') }}–{{ schedule.tasks[1].end + | as_datetime | as_local | as_timestamp + | timestamp_custom('%H:%M') }} + ({{ 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). diff --git a/docs/user/docs/scheduling-actions.md b/docs/user/docs/scheduling-actions.md index a28e41d..6dcb169 100644 --- a/docs/user/docs/scheduling-actions.md +++ b/docs/user/docs/scheduling-actions.md @@ -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
@@ -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.