mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
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:
parent
31fca73ccd
commit
a8d1519a26
2 changed files with 55 additions and 52 deletions
|
|
@ -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 }}"
|
||||||
|
|
||||||
# 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:
|
|
||||||
- service: input_datetime.set_datetime
|
- service: input_datetime.set_datetime
|
||||||
target:
|
target:
|
||||||
entity_id: input_datetime.dryer_start
|
entity_id: input_datetime.dryer_start
|
||||||
data:
|
data:
|
||||||
datetime: "{{ dryer_result.window.start }}"
|
datetime: "{{ schedule.tasks[1].start }}"
|
||||||
- service: notify.mobile_app
|
- service: notify.mobile_app
|
||||||
data:
|
data:
|
||||||
title: "🧺 Laundry Planned"
|
title: "🧺 Laundry Planned"
|
||||||
message: >
|
message: >
|
||||||
Washing: {{ washer_result.window.start | as_datetime
|
Washing: {{ schedule.tasks[0].start | as_datetime
|
||||||
| as_local | as_timestamp
|
| 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
|
| as_datetime | as_local | as_timestamp
|
||||||
| timestamp_custom('%H:%M') }}
|
| timestamp_custom('%H:%M') }}
|
||||||
({{ washer_result.window.price_mean | round(1) }}
|
({{ schedule.tasks[0].price_mean | round(1) }}
|
||||||
{{ washer_result.price_unit }})
|
{{ schedule.price_unit }})
|
||||||
Dryer: {{ dryer_result.window.start | as_datetime
|
Dryer: {{ schedule.tasks[1].start | as_datetime
|
||||||
| as_local | as_timestamp
|
| 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
|
| as_datetime | as_local | as_timestamp
|
||||||
| timestamp_custom('%H:%M') }}
|
| timestamp_custom('%H:%M') }}
|
||||||
({{ dryer_result.window.price_mean | round(1) }}
|
({{ schedule.tasks[1].price_mean | round(1) }}
|
||||||
{{ dryer_result.price_unit }})
|
{{ schedule.price_unit }})
|
||||||
|
Total: {{ schedule.total_estimated_cost | round(2) }}
|
||||||
|
{{ schedule.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).
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue