Skip to main content
Version: Next 🚧

Scheduling Actions

Find the cheapest (or most expensive) time windows for your appliances β€” automatically. These actions analyze real Tibber price data and return optimal scheduling recommendations.

Entity ID tip

<home_name> is a placeholder for your Tibber home display name in Home Assistant. Entity IDs are derived from the displayed name (localized), so the exact slug may differ. Can't find a sensor? Use the Entity Reference (All Languages) to search by name in your language.

Overview​

ActionWhat It DoesBest For
find_cheapest_blockFinds the cheapest contiguous time windowDishwasher, washing machine, dryer
find_cheapest_hoursFinds the cheapest intervals (can be non-contiguous)EV charging, battery storage, water heater
find_cheapest_scheduleSchedules multiple appliances without overlapDishwasher + washing machine + dryer overnight
find_most_expensive_blockFinds the most expensive contiguous windowAvoid running appliances during peak prices
find_most_expensive_hoursFinds the most expensive intervalsBattery discharge optimization, peak avoidance

Choosing the Right Action​

Rules of thumb:

  • 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) β†’ find_cheapest_schedule with sequential: true (guaranteed order + gap)
  • "When should I NOT run this?" β†’ find_most_expensive_block or find_most_expensive_hours

Search Range​

All scheduling actions share the same flexible search range options. You can define when to look for cheap prices in several ways:

Quick Scopes​

The simplest approach β€” use search_scope for common time ranges:

ScopeStartEnd
today00:00 today00:00 tomorrow
tomorrow00:00 tomorrow00:00 day after tomorrow
remaining_todayNow00:00 tomorrow
next_24hNowNow + 24 hours
next_48hNowNow + 48 hours
Show YAML: Quick Scopes
service: tibber_prices.find_cheapest_block
data:
duration: "02:00:00"
search_scope: tomorrow

Explicit Start/End​

For full control, specify exact datetime values:

Show YAML: Explicit Start and End
service: tibber_prices.find_cheapest_block
data:
duration: "02:00:00"
search_start: "2026-04-11T22:00:00+02:00"
search_end: "2026-04-12T06:00:00+02:00"

Time-of-Day with Day Offset​

Schedule relative to today using time + day offset:

Show YAML: Time of Day with Offset
service: tibber_prices.find_cheapest_block
data:
duration: "02:00:00"
search_start_time: "22:00:00" # 22:00 today
search_end_time: "06:00:00"
search_end_day_offset: 1 # 06:00 tomorrow

Minute Offsets from Now​

For relative searches:

Show YAML: Relative Minute Offsets
service: tibber_prices.find_cheapest_block
data:
duration: "01:30:00"
search_start_offset_minutes: 0 # Starting now
search_end_offset_minutes: 480 # Next 8 hours

Default Behavior​

If you omit all range parameters, the search covers now until the end of tomorrow β€” the maximum window with available price data.

Don't mix scopes with explicit ranges

search_scope cannot be combined with explicit range parameters (search_start, search_end, etc.). Use one approach or the other.


Common Parameters​

These parameters are available across all scheduling actions:

ParameterDescriptionDefault
entry_idConfig entry ID. Auto-selects if you only have one home.Auto
include_current_intervalInclude the currently running 15-minute interval in the search?true
min_price_levelOnly consider intervals at or above this Tibber levelβ€”
max_price_levelOnly consider intervals at or below this Tibber levelβ€”
smooth_outliersSmooth price outliers before searching (see below)true
min_distance_from_avgRequire result to differ from average by X% (see below)β€”
allow_relaxationProgressively loosen filters to guarantee a result (see below)true
duration_flexibility_minutesMax minutes the duration may be shortened during relaxation (see below)Auto
power_profileWatt values per 15-min interval for accurate cost estimatesβ€”
use_base_unitUse base currency (EUR, NOK) instead of subunit (ct, ΓΈre)false
min_distance_from_avg availability

min_distance_from_avg is available in find_cheapest_block, find_most_expensive_block, find_cheapest_hours, and find_most_expensive_hours. It is not available in find_cheapest_schedule (multi-task semantics make a single threshold ambiguous).

Price Level Filtering​

Restrict the search to specific Tibber price levels. Levels from lowest to highest: very_cheap, cheap, normal, expensive, very_expensive.

Show YAML: Price Level Filtering
# Only search within cheap or very cheap intervals
service: tibber_prices.find_cheapest_block
data:
duration: "02:00:00"
search_scope: next_24h
max_price_level: cheap # Exclude normal, expensive, very_expensive

Power Profile​

By default, cost estimates assume a constant 1 kW load. If your appliance has variable power draw, provide a power profile β€” one watt value per 15-minute interval:

Show YAML: Power Profile
# Washing machine: high power for heating, then less
service: tibber_prices.find_cheapest_block
data:
duration: "01:30:00" # 6 intervals Γ— 15 min
power_profile:
- 2200 # Interval 1: Heating water (2.2 kW)
- 2200 # Interval 2: Heating continues
- 800 # Interval 3: Washing cycle
- 800 # Interval 4: Washing cycle
- 1500 # Interval 5: Spin cycle
- 500 # Interval 6: Final rinse

The service then calculates estimated_total_cost using the actual power draw per interval instead of flat 1 kW, and adds estimated_load_kwh (total energy consumed) to the response.

Duration and profile must match

The number of entries in power_profile must exactly match the number of 15-minute intervals in duration. A 2-hour duration needs 8 entries.

Outlier Smoothing​

Enabled by default. A single extreme price spike (or dip) can pull the "cheapest" window away from a genuinely good period. With smooth_outliers: true, outlier intervals are temporarily replaced by the average of their neighbors before the search runs. The response always shows original (unsmoothed) prices β€” smoothing only affects which window is selected.

Set smooth_outliers: false to disable:

service: tibber_prices.find_cheapest_block
data:
duration: "02:00:00"
search_scope: next_24h
smooth_outliers: false

Minimum Distance from Average​

Opt-in quality gate. Ensures the found result is meaningfully different from the search-range average β€” not just "the cheapest, but still close to average".

  • For cheapest: the result must be at least X% below the average.
  • For most expensive: the result must be at least X% above the average.

If the condition is not met, no result is returned and the reason field explains why (window_above_distance_threshold for blocks, selection_above_distance_threshold for hours β€” or ..._below_... for most expensive).

# Only return a result if it's at least 10% cheaper than average
service: tibber_prices.find_cheapest_block
data:
duration: "02:00:00"
search_scope: today
min_distance_from_avg: 10.0
When to use min_distance_from_avg

On days with flat prices (all intervals nearly the same), the "cheapest" window may only be marginally cheaper than any other. Use min_distance_from_avg to avoid scheduling an appliance for negligible savings β€” and instead run it whenever convenient.

Coefficient of Variation​

All scheduling action responses include a coefficient_of_variation field in their statistics. This measures the relative price spread within the found window (standard deviation Γ· mean, as a ratio). A low value (e.g., 0.05) means prices are very uniform; a high value (e.g., 0.30) means prices vary significantly within the window.

You can use this in automations to decide whether the found window is "good enough":

# Only start if prices within the window are reasonably uniform
condition: template
value_template: >
{{ action_response.window.coefficient_of_variation < 0.15 }}

Relaxation​

Enabled by default. Relaxation ensures you always get a result, even when your filters are too strict for the available price data. Without relaxation, a tight max_price_level or min_distance_from_avg could return nothing β€” leaving your automation without a plan. With relaxation, the service progressively loosens constraints until a window is found.

How it works:

Relaxation proceeds in three phases, stopping as soon as a result is found:

  1. Distance relaxation β€” Halves min_distance_from_avg, then removes it entirely
  2. Level filter relaxation β€” Gradually widens the allowed price level range (e.g., very_cheap β†’ cheap β†’ normal β†’ any)
  3. Duration reduction β€” Shortens the duration by one interval (15 min) per step, down to a minimum of 30 minutes

Each phase tries the least invasive change first. If phase 1 produces a result, phases 2 and 3 are never attempted.

When does relaxation activate?

Relaxation only activates when the original parameters return no result. If your filters already find a window, relaxation does nothing β€” there is zero overhead.

Parameters:

ParameterTypeDefaultDescription
allow_relaxationbooleantrueEnable progressive filter relaxation. Set to false for strict mode (fail if nothing matches).
duration_flexibility_minutesnumberAutoMaximum minutes the duration may be shortened (0–120, in 15-min steps). If omitted, calculated automatically based on the requested duration.
# Opt out of relaxation (strict mode)
service: tibber_prices.find_cheapest_block
data:
duration: "02:00:00"
search_scope: next_24h
allow_relaxation: false
# Allow up to 30 min shorter than requested
service: tibber_prices.find_cheapest_block
data:
duration: "02:00:00"
search_scope: next_24h
duration_flexibility_minutes: 30

Response metadata:

When relaxation is applied, the response includes extra fields:

FieldDescription
relaxation_appliedtrue if filters were relaxed to find the result, false if original parameters succeeded
relaxation_stepsNumber of relaxation steps applied (only present when relaxation_applied is true)
duration_minutesEffective duration (may be shorter than duration_minutes_requested if duration was reduced)

If all relaxation steps are exhausted without finding a result, the response still indicates failure with reason: "relaxation_exhausted".

# Check if relaxation was needed in your automation
- if: "{{ result.window_found and not result.relaxation_applied }}"
then:
- service: notify.mobile_app
data:
message: "Found optimal window at original settings"
- if: "{{ result.window_found and result.relaxation_applied }}"
then:
- service: notify.mobile_app
data:
message: >
Found window after {{ result.relaxation_steps }} relaxation steps.
Effective duration: {{ result.duration_minutes }} min
(requested: {{ result.duration_minutes_requested }} min)
Schedule service differences

For find_cheapest_schedule, relaxation works slightly differently: phase 1 (distance) is skipped because the schedule service does not support min_distance_from_avg. Level filter relaxation and duration reduction apply to all tasks uniformly.


Find Cheapest Block​

Finds the single cheapest contiguous time window of a given duration.

Use when: Your appliance must run uninterrupted for a fixed time.

Basic Example​

Show YAML: Find Cheapest Block
service: tibber_prices.find_cheapest_block
data:
duration: "02:00:00"
search_scope: next_24h
response_variable: result

Example with All Options​

Show YAML: Cheapest Block with All Options
service: tibber_prices.find_cheapest_block
data:
duration: "02:00:00"
search_scope: next_24h
max_price_level: normal
power_profile: [2200, 2200, 800, 800, 1500, 500, 400, 200]
include_comparison_details: true
include_current_interval: false
response_variable: result

Response​

Show JSON: Cheapest Block Example Response
{
"home_id": "abc-123",
"search_start": "2026-04-11T14:00:00+02:00",
"search_end": "2026-04-12T14:00:00+02:00",
"duration_minutes_requested": 120,
"duration_minutes": 120,
"currency": "EUR",
"price_unit": "ct/kWh",
"window_found": true,
"window": {
"start": "2026-04-12T02:00:00+02:00",
"end": "2026-04-12T04:00:00+02:00",
"duration_minutes": 120,
"interval_count": 8,
"price_mean": 14.25,
"price_median": 13.90,
"price_min": 12.00,
"price_max": 16.80,
"price_spread": 4.80,
"estimated_total_cost": 28.50,
"intervals": [
{
"starts_at": "2026-04-12T02:00:00+02:00",
"ends_at": "2026-04-12T02:15:00+02:00",
"price": 12.00,
"level": "very_cheap",
"rating_level": "low"
}
]
},
"price_comparison": {
"comparison_price_mean": 32.10,
"price_difference": 17.85,
"comparison_window_start": "2026-04-11T18:00:00+02:00"
}
}

Key response fields:

FieldDescription
window_foundtrue if a window was found, false if no intervals match the criteria
window.start / window.endWhen to start and stop the appliance
window.price_meanAverage price during the window
window.estimated_total_costEstimated cost (assumes 1 kW unless power_profile provided)
price_comparisonHow this window compares to the most expensive alternative
price_comparison.price_differenceHow much cheaper this window is vs. the most expensive option
relaxation_appliedtrue if relaxation was needed to find the result
relaxation_stepsNumber of relaxation steps applied (only when relaxation_applied is true)
duration_minutesEffective duration β€” may differ from duration_minutes_requested after relaxation

Use in Automations​

Prerequisite: Create an input_datetime.dishwasher_start helper (type: Date and time) in Settings β†’ Helpers.

Show YAML: Dishwasher Automation (Plan + Execute)
automation:
# Step 1: Plan the cheapest start time every evening
- alias: "Dishwasher - Plan Cheapest Start"
trigger:
- platform: time
at: "20:00:00"
action:
- service: tibber_prices.find_cheapest_block
data:
duration: "02:00:00"
search_start_time: "20:00:00"
search_end_time: "06:00:00"
search_end_day_offset: 1
response_variable: result
- if: "{{ result.window_found }}"
then:
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.dishwasher_start
data:
datetime: "{{ result.window.start }}"

# Step 2: Start the dishwasher at the stored time (survives HA restarts)
- alias: "Dishwasher - Start at Planned Time"
trigger:
- platform: time
at: input_datetime.dishwasher_start
action:
# Option A: Smart appliance via Home Connect (see tip below)
# Option B: Smart plug
- service: switch.turn_on
target:
entity_id: switch.dishwasher_smart_plug
Home Connect instead of smart plugs

If you have a Bosch/Siemens appliance with Home Connect, you can start the program directly instead of using a smart plug. See Automation Examples β€” Home Connect tip for exact service call syntax for both the official and alternative integration.


Find Cheapest Hours​

Finds the cheapest N minutes of intervals within a search range. Intervals do not need to be contiguous β€” the service picks the cheapest individual 15-minute slots and groups them into segments.

Use when: Your device can pause and resume freely (EV charger, battery storage, pool pump).

Basic Example​

Show YAML: Find Cheapest Hours
service: tibber_prices.find_cheapest_hours
data:
duration: "04:00:00"
search_scope: next_24h
response_variable: result

With Minimum Segment Duration​

Some devices shouldn't cycle on/off too rapidly. Use min_segment_duration to ensure each contiguous run is at least a minimum length:

Show YAML: With Minimum Segment Duration
# EV charger: 3 hours total, but each charging session at least 30 min
service: tibber_prices.find_cheapest_hours
data:
duration: "03:00:00"
min_segment_duration: "00:30:00"
search_scope: next_24h
response_variable: result

Response​

Show JSON: Cheapest Hours Example Response
{
"home_id": "abc-123",
"search_start": "2026-04-11T14:00:00+02:00",
"search_end": "2026-04-12T14:00:00+02:00",
"total_minutes_requested": 240,
"total_minutes": 240,
"currency": "EUR",
"price_unit": "ct/kWh",
"intervals_found": true,
"schedule": {
"total_minutes": 240,
"interval_count": 16,
"price_mean": 13.50,
"price_median": 13.20,
"price_min": 10.80,
"price_max": 16.30,
"price_spread": 5.50,
"estimated_total_cost": 54.00,
"segment_count": 3,
"segments": [
{
"start": "2026-04-11T23:00:00+02:00",
"end": "2026-04-12T00:30:00+02:00",
"duration_minutes": 90,
"interval_count": 6,
"price_mean": 11.20,
"intervals": []
},
{
"start": "2026-04-12T02:00:00+02:00",
"end": "2026-04-12T03:15:00+02:00",
"duration_minutes": 75,
"interval_count": 5,
"price_mean": 12.80,
"intervals": []
},
{
"start": "2026-04-12T05:00:00+02:00",
"end": "2026-04-12T06:15:00+02:00",
"duration_minutes": 75,
"interval_count": 5,
"price_mean": 16.00,
"intervals": []
}
],
"intervals": []
},
"price_comparison": {
"comparison_price_mean": 28.50,
"price_difference": 15.00
}
}

Key response fields:

FieldDescription
intervals_foundtrue if enough cheap intervals were found
schedule.segment_countHow many separate contiguous runs the schedule has
schedule.segments[]Each continuous "on" period with its own start/end and price stats
schedule.intervals[]All selected intervals in chronological order
relaxation_appliedtrue if relaxation was needed to find the result
relaxation_stepsNumber of relaxation steps applied (only when relaxation_applied is true)
total_minutesEffective total duration β€” may differ from total_minutes_requested after relaxation

Find Cheapest Schedule​

Schedules multiple appliances within the same search range, ensuring they don't overlap. Each appliance gets its own cheapest contiguous time window.

Use when: You have multiple appliances sharing a circuit or you want to avoid running them at the same time (e.g., limited main fuse capacity).

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​

Show YAML: Find Cheapest Schedule
service: tibber_prices.find_cheapest_schedule
data:
tasks:
- name: dishwasher
duration: "02:00:00"
- name: washing_machine
duration: "01:30:00"
search_scope: next_24h
response_variable: result

With Gap and Power Profiles​

Show YAML: With Gap and Power Profiles
service: tibber_prices.find_cheapest_schedule
data:
tasks:
- name: dishwasher
duration: "02:00:00"
power_profile: [2200, 2200, 800, 800, 1500, 500, 400, 200]
- name: washing_machine
duration: "01:30:00"
power_profile: [2000, 2000, 800, 800, 1200, 500]
- name: dryer
duration: "01:00:00"
power_profile: [2500, 2500, 2000, 1500]
gap_minutes: 15
search_start_time: "22:00:00"
search_end_time: "07:00:00"
search_end_day_offset: 1
response_variable: result

Response​

Show JSON: Cheapest Schedule Example Response
{
"home_id": "abc-123",
"search_start": "2026-04-11T22:00:00+02:00",
"search_end": "2026-04-12T07:00:00+02:00",
"currency": "EUR",
"price_unit": "ct/kWh",
"all_tasks_scheduled": true,
"unscheduled_tasks": null,
"tasks": [
{
"name": "dishwasher",
"start": "2026-04-12T00:00:00+02:00",
"end": "2026-04-12T02:00:00+02:00",
"duration_minutes_requested": 120,
"duration_minutes": 120,
"price_mean": 12.30,
"price_median": 12.10,
"price_min": 10.50,
"price_max": 14.20,
"price_spread": 3.70,
"estimated_total_cost": 24.60,
"intervals": []
},
{
"name": "washing_machine",
"start": "2026-04-12T02:15:00+02:00",
"end": "2026-04-12T03:45:00+02:00",
"duration_minutes_requested": 90,
"duration_minutes": 90,
"price_mean": 13.80,
"price_median": 13.50,
"price_min": 12.00,
"price_max": 16.10,
"price_spread": 4.10,
"estimated_total_cost": 20.70,
"intervals": []
},
{
"name": "dryer",
"start": "2026-04-12T04:00:00+02:00",
"end": "2026-04-12T05:00:00+02:00",
"duration_minutes_requested": 60,
"duration_minutes": 60,
"price_mean": 14.50,
"price_median": 14.30,
"price_min": 13.80,
"price_max": 15.40,
"price_spread": 1.60,
"estimated_total_cost": 14.50,
"intervals": []
}
],
"total_estimated_cost": 59.80
}

Key response fields:

FieldDescription
all_tasks_scheduledtrue if every task found a slot, false if some couldn't fit
unscheduled_tasksList of task names that couldn't be placed (or null if all succeeded)
tasks[]Each task with its assigned time window and price statistics
tasks[].start / tasks[].endWhen to start and stop each appliance
total_estimated_costCombined cost across all tasks
relaxation_appliedtrue if relaxation was needed to schedule all tasks
relaxation_stepsNumber of relaxation steps applied (only when relaxation_applied is true)

Why Not Just Call find_cheapest_block Multiple Times?​

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.

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 for a complete example.

Gap Minutes​

Use gap_minutes to add a mandatory pause between appliances:

  • Shared plumbing: 15 min gap between dishwasher and washing machine
  • Circuit protection: 30 min gap to let cables cool down
  • Heat pump compressor: 15–30 min cool-down between cycles

The gap is rounded up to the nearest 15 minutes (quarter-hour granularity).


Find Most Expensive Block​

The opposite of find_cheapest_block β€” finds the most expensive contiguous window.

Parameters: Identical to find_cheapest_block.

Response: Same structure. The price_comparison compares against the cheapest block.

Show YAML: Find Most Expensive Block
service: tibber_prices.find_most_expensive_block
data:
duration: "02:00:00"
search_scope: tomorrow
response_variable: peak

Use cases:

  • "When should I definitely NOT run my washing machine?"
  • Schedule battery discharge during peak prices
  • Send notifications before expensive periods start

Find Most Expensive Hours​

The opposite of find_cheapest_hours β€” finds the most expensive intervals (non-contiguous).

Parameters: Identical to find_cheapest_hours (including min_segment_duration).

Response: Same structure. The price_comparison compares against the cheapest hours.

Show YAML: Find Most Expensive Hours
service: tibber_prices.find_most_expensive_hours
data:
duration: "04:00:00"
search_scope: tomorrow
response_variable: peak

Use cases:

  • Battery discharge optimization: sell stored energy during the most expensive 4 hours
  • Demand response: reduce consumption during the most expensive periods
  • Peak avoidance alerts: notify before expensive intervals start

Practical Examples​

Restart-safe automations

All examples below use input_datetime helpers to store planned start times. This ensures your schedule survives HA restarts β€” unlike delay or wait_for_trigger which are lost on restart.

Setup: Create an input_datetime helper per appliance in Settings β†’ Devices & Services β†’ Helpers β†’ Create Helper β†’ Date and/or time (choose "Date and time").

Overnight Appliance Scheduling​

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.

Sequential appliances (e.g., washer β†’ dryer)?

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.

Prerequisites: Create input_datetime.dishwasher_start and input_datetime.washing_machine_start helpers.

Show YAML: Overnight Appliance Scheduling (Plan + Execute)
automation:
# Planning automation β€” runs every evening
- alias: "Laundry - Plan Overnight Schedule"
trigger:
- platform: time
at: "21:00:00"
action:
- service: tibber_prices.find_cheapest_schedule
data:
tasks:
- name: dishwasher
duration: "02:00:00"
- name: washing_machine
duration: "01:30:00"
gap_minutes: 15
search_start_time: "22:00:00"
search_end_time: "06:00:00"
search_end_day_offset: 1
response_variable: schedule
- if: "{{ schedule.all_tasks_scheduled }}"
then:
# Store start times in helpers (survives HA restarts)
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.dishwasher_start
data:
datetime: >
{{ schedule.tasks | selectattr('name', 'eq', 'dishwasher')
| map(attribute='start') | first }}
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.washing_machine_start
data:
datetime: >
{{ schedule.tasks | selectattr('name', 'eq', 'washing_machine')
| map(attribute='start') | first }}
- service: notify.mobile_app
data:
title: "🧺 Laundry Planned"
message: >
Dishwasher: {{ (schedule.tasks | selectattr('name', 'eq', 'dishwasher')
| map(attribute='start') | first) | as_datetime | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
Washing machine: {{ (schedule.tasks | selectattr('name', 'eq', 'washing_machine')
| map(attribute='start') | first) | as_datetime | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
Total cost: ~{{ schedule.total_estimated_cost | round(1) }}
{{ schedule.currency }}

# Execution automations β€” trigger at stored times
- alias: "Dishwasher - Start at Planned Time"
trigger:
- platform: time
at: input_datetime.dishwasher_start
action:
# Use Home Connect or smart plug β€” see automation-examples.md
- service: switch.turn_on
target:
entity_id: switch.dishwasher_smart_plug

- alias: "Washing Machine - Start at Planned Time"
trigger:
- platform: time
at: input_datetime.washing_machine_start
action:
- service: switch.turn_on
target:
entity_id: switch.washing_machine_smart_plug

EV Charging During Cheapest 4 Hours​

Prerequisite: Create input_datetime.ev_charge_start helper.

Show YAML: EV Charging in Cheapest 4 Hours
automation:
- alias: "EV - Plan Cheapest Charging"
trigger:
- platform: time
at: "18:00:00"
condition:
- condition: numeric_state
entity_id: sensor.ev_battery_level
below: 80
action:
- service: tibber_prices.find_cheapest_hours
data:
duration: "04:00:00"
min_segment_duration: "00:30:00"
search_start_time: "18:00:00"
search_end_time: "07:00:00"
search_end_day_offset: 1
response_variable: charging
- if: "{{ charging.intervals_found }}"
then:
# Store first segment start time
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.ev_charge_start
data:
datetime: "{{ charging.schedule.segments[0].start }}"
- service: notify.mobile_app
data:
title: "πŸ”Œ EV Charging Planned"
message: >
{{ charging.schedule.segment_count }} sessions:
{% for seg in charging.schedule.segments %}
β€’ {{ seg.start | as_datetime | as_local
| as_timestamp | timestamp_custom('%H:%M') }}–{{ seg.end
| as_datetime | as_local | as_timestamp
| timestamp_custom('%H:%M') }}
({{ seg.price_mean | round(1) }} {{ charging.price_unit }})
{% endfor %}
Savings vs. peak: {{ charging.price_comparison.price_difference
| round(1) }} {{ charging.price_unit }}
Simpler alternative for EV charging

If your charger can't pause/resume, use find_cheapest_block instead for one contiguous window. See the Automation Examples for a complete example.

Peak Price Warning​

Show YAML: Peak Price Warning
automation:
- alias: "Peak Price - Morning Warning"
trigger:
- platform: time
at: "07:00:00"
action:
- service: tibber_prices.find_most_expensive_block
data:
duration: "02:00:00"
search_scope: today
response_variable: peak
- if: "{{ peak.window_found }}"
then:
- service: notify.mobile_app
data:
title: "⚑ Expensive Period Today"
message: >
Avoid heavy loads between
{{ peak.window.start | as_datetime | as_local
| as_timestamp | timestamp_custom('%H:%M') }}
and {{ peak.window.end | as_datetime | as_local
| as_timestamp | timestamp_custom('%H:%M') }}.
Average price: {{ peak.window.price_mean | round(1) }}
{{ peak.price_unit }}

Technical Notes​

Duration Rounding​

All durations are rounded up to the nearest 15 minutes because Tibber price data has quarter-hourly resolution. A 20-minute duration becomes 30 minutes (2 intervals). A 2-hour duration stays at 120 minutes (8 intervals).

Comparison Details​

Add include_comparison_details: true to find_cheapest_block or find_cheapest_hours to get extra fields in the comparison:

Show YAML: Comparison Details
service: tibber_prices.find_cheapest_block
data:
duration: "02:00:00"
include_comparison_details: true

This adds comparison_price_min, comparison_price_max, and comparison_window_end to the price_comparison object.

Response When No Window Found​

If no intervals match your criteria (e.g., the search range is too short, all intervals are filtered out by price level), the response indicates failure:

  • find_cheapest_block: "window_found": false, "window": null
  • find_cheapest_hours: "intervals_found": false, "schedule": null
  • find_cheapest_schedule: "all_tasks_scheduled": false, "unscheduled_tasks": ["task_name"]

The reason field contains a stable machine-readable code you can use in automations:

Reason CodeMeaning
no_data_in_rangeNo price data available for the search range
no_intervals_matching_level_filterLevel filter excluded all intervals
insufficient_intervals_after_filterNot enough intervals left after filtering
insufficient_intervals_for_constraintsEnough intervals, but constraints (min segment) can't be met
window_above_distance_thresholdBlock found, but not far enough below average (min_distance_from_avg)
window_below_distance_thresholdMost expensive block found, but not far enough above average
selection_above_distance_thresholdHours found, but not far enough below average (min_distance_from_avg)
selection_below_distance_thresholdMost expensive hours found, but not far enough above average
relaxation_exhaustedAll relaxation steps tried, still no result (only when allow_relaxation: true)

Always check the failure fields in your automations before using the results.

πŸ’¬ Comments are page-specific. For a new question or idea, open a dedicated Discussion on GitHub so it gets its own thread and proper visibility.