To find the cheapest electricity hours in Home Assistant, compare complete price intervals across the time when you can actually run the task. For a job that must finish without interruption, choose the lowest average price across a continuous block. Start by showing that block as a reminder; only consider automatic starting after checking your appliance’s supported controls.

This walkthrough uses Home Assistant’s built-in Nord Pool integration for a supported European market and a suitable spot-price contract. If your supplier already provides a documented cheapest-window action, use that instead of rebuilding it. If your tariff has fixed off-peak hours, the Schedule helper guide is the simpler starting point.

Researched from current Home Assistant documentation on November 4, 2026. The calculation was checked offline with synthetic data; no live Home Assistant or appliance tests were performed. The price example is hypothetical, with no measured savings claim. The cover is an AI-generated illustration.

1. Check whether the price data matches your bill

A spot price is the market price for electricity in a particular place and interval. Your retail bill may add taxes, network charges and supplier fees. An integration is Home Assistant’s connection to that data source. Nord Pool supplies base market prices; it does not know every charge in your contract. See the official integration documentation.

On a phone, swipe sideways inside the tables.

Your contractUseful approachDecision before proceeding
Same price per kWh all daySchedule for convenience.Moving the same usage to another hour does not reduce its energy charge.
Fixed peak and off-peak periodsUse the supplier’s timetable.Check weekday, weekend and seasonal differences.
Market-linked price plus a constant per-kWh fee and percentage taxA spot-price comparison can identify the cheapest block.Those constant adjustments preserve the ordering, but the displayed amount is not your final price.
Time-varying network fees, export opportunity costs or other changing chargesCompare the relevant combined cost.The raw-price example below may choose the wrong block for your household.

For example, adding 3 cents to every kWh and applying the same positive tax multiplier preserves the ordering for equal consumption. Adding 10 cents only during certain hours can change the winner. This guide does not optimize demand charges, home batteries or solar export.

2. Compare a whole run, not isolated cheap hours

Suppose a task can run between 8 a.m. and noon and needs two uninterrupted hours. These are invented prices for explanation, in euro cents per kWh:

IntervalPriceCan it stand alone as a two-hour run?
08:00–09:004 cents/kWhNo; the following hour costs 30 cents.
09:00–10:0030 cents/kWhNo; compare it with the following hour.
10:00–11:005 cents/kWhTogether with 11:00–12:00, yes.
11:00–12:006 cents/kWhThere is no later hour in the allowed window.

The two cheapest isolated hours are 08:00 and 10:00, with a gap between them. For continuous operation, compare these three choices:

  • 08:00–10:00: (4 + 30) ÷ 2 = 17 cents/kWh.
  • 09:00–11:00: (30 + 5) ÷ 2 = 17.5 cents/kWh.
  • 10:00–12:00: (5 + 6) ÷ 2 = 5.5 cents/kWh — the lowest continuous block.

A hypothetical constant 1 kW load uses 2 kWh in two hours. Its energy charge in those blocks would be €0.34, €0.35 and €0.11, before other charges. A dishwasher’s power varies during its cycle: this average-price method is a planning approximation, not a calculation of that appliance’s exact cost. Choose a duration that covers the actual program; a two-hour plan does not cover a three-hour wash.

With quarter-hour prices, two hours normally spans eight intervals. The example below compares timestamps and durations rather than assuming each list entry represents an hour. It considers starts on returned interval boundaries.

3. Confirm you can retrieve the right prices

  1. Check prerequisites. You need a running Home Assistant installation, permission to add integrations and scripts, internet access, and a market/contract for which these prices are relevant. No smart plug, energy meter, paid Home Assistant Cloud plan or additional app is required for this preview.
  2. Add Nord Pool through Settings → Devices & services → Add integration. Choose your actual market area and currency. This example uses FI and EUR; FI is Finland’s market area, not a universal default.
  3. Open the Actions tool. Current documentation calls this Settings → Tools → Actions; older releases may show Developer Tools → Actions. Select Nord Pool: Get prices for date, your configuration entry, tomorrow’s date, your area and EUR.
  4. Perform that read-only action. Inspect the response: the area name contains a list of start, end and price values. Switch the action to YAML view and copy its config_entry value for the script below. This identifies the installed integration, not a sensor.

The date-specific action documentation specifies UTC timestamps and prices per MWh. Divide by 1,000 to display the same currency per kWh: 55 EUR/MWh becomes 0.055 EUR/kWh, or 5.5 euro cents/kWh. Do not apply that conversion again to an existing sensor already expressed in EUR/kWh.

Tomorrow’s prices must have been published. The documentation gives a typical release around 13:00 CET/CEST, not a guaranteed deadline. If the request fails, retry later and inspect the error. A missing price is not a free interval.

4. Create a notification-only planner

Create a new script under Settings → Automations & scenes → Scripts, open its menu and choose Edit in YAML. Paste the complete block below into that individual script editor. Replace REPLACE_WITH_YOUR_CONFIG_ENTRY with the value you copied, and change area to your own market.

Keep EUR for this example so the output label stays correct. Check Home Assistant’s configured time zone: the allowed window is tomorrow, 08:00–22:00 in that time zone. The duration is 7200 seconds, or two elapsed hours. The script accepts 15-minute and hourly intervals, requires complete coverage of the allowed window, and keeps the earlier start when prices tie.

Complete script: compare prices and show one plan

Scroll sideways inside the code box to read long lines; copy the entire block.

alias: "Electricity: tomorrow's two-hour plan"
mode: single
sequence:
  - variables:
      area: "FI"
      plan_date: "{{ (now().date() + timedelta(days=1)).isoformat() }}"
      window_start: "{{ as_timestamp(today_at('08:00') + timedelta(days=1)) }}"
      window_end: "{{ as_timestamp(today_at('22:00') + timedelta(days=1)) }}"
      run_seconds: 7200
  - action: persistent_notification.create
    data:
      notification_id: electricity_window_plan
      title: "Electricity plan for {{ plan_date }}"
      message: >-
        No price plan is ready. Fetching prices now.
        If this message remains, check the script trace and try later.
        No appliance has been scheduled.
  - action: nordpool.get_prices_for_date
    data:
      config_entry: "REPLACE_WITH_YOUR_CONFIG_ENTRY"
      date: "{{ plan_date }}"
      areas:
        - "{{ area }}"
      currency: "EUR"
    response_variable: prices
  - variables:
      plan: >-
        {% set begin = window_start | float %}
        {% set finish = window_end | float %}
        {% set duration = run_seconds | int %}
        {% set ns = namespace(rows=[], valid=true) %}
        {% for row in prices.get(area, []) %}
          {% set s = as_timestamp(row.start, none) %}
          {% set e = as_timestamp(row.end, none) %}
          {% if s is none or e is none or not is_number(row.price) %}
            {% set ns.valid = false %}
          {% elif s < finish and e > begin %}
            {% if s < begin or e > finish or e - s not in [900, 3600] %}
              {% set ns.valid = false %}
            {% endif %}
            {% set ns.rows = ns.rows + [dict(s=s, e=e, p=row.price | float)] %}
          {% endif %}
        {% endfor %}
        {% set rows = ns.rows | sort(attribute='s') %}
        {% set cursor = namespace(next=begin) %}
        {% for row in rows %}
          {% if row.s != cursor.next %}
            {% set ns.valid = false %}
          {% endif %}
          {% set cursor.next = row.e %}
        {% endfor %}
        {% if cursor.next != finish %}
          {% set ns.valid = false %}
        {% endif %}
        {% set best = namespace(start=none, end=none, mean=none) %}
        {% if ns.valid %}
          {% for first in rows %}
            {% set stop = first.s + duration %}
            {% set win = namespace(weighted=0, seconds=0) %}
            {% for row in rows if row.s >= first.s and row.e <= stop %}
              {% set seconds = row.e - row.s %}
              {% set win.seconds = win.seconds + seconds %}
              {% set win.weighted = win.weighted + row.p * seconds %}
            {% endfor %}
            {% if win.seconds == duration and stop <= finish %}
              {% set mean = win.weighted / duration %}
              {% if best.mean is none or mean < best.mean %}
                {% set best.start = first.s %}
                {% set best.end = stop %}
                {% set best.mean = mean %}
              {% endif %}
            {% endif %}
          {% endfor %}
        {% endif %}
        {{ dict(valid=best.start is not none,
                start=best.start, end=best.end,
                price=(best.mean / 1000) if best.mean is not none else none) }}
  - action: persistent_notification.create
    data:
      notification_id: electricity_window_plan
      title: "Electricity plan for {{ plan_date }}"
      message: >-
        {% if plan.valid %}
          Suggested window:
          {{ plan.start | timestamp_custom('%Y-%m-%d %H:%M %Z', true) }}
          to {{ plan.end | timestamp_custom('%H:%M %Z', true) }}.
          Average spot price: {{ plan.price | round(4) }} EUR/kWh.
          Charges and VAT excluded. Check your tariff and the appliance's
          supported delayed-start settings. No appliance has been scheduled.
        {% else %}
          No plan: no complete run fits, or the allowed window has missing
          or unsupported data.
          Check the area, dates, interval lengths and returned prices.
          No appliance has been scheduled.
        {% endif %}

The first message replaces any previous plan. If fetching fails, Home Assistant’s normal stop-on-error behavior leaves that “No price plan is ready” notice. It does not keep presenting yesterday’s suggestion as a fresh result. A successful request with gaps, overlapping intervals or unsupported interval lengths produces an explicit “No plan” message.

The calculation sorts the returned intervals, checks that they join up across the whole allowed window, then compares each complete two-hour block. Longer intervals receive proportionally more weight. This is an original planning example using template loops and namespaces, with timestamps for comparison and local time for display. It does not assume every day contains 24 hourly rows.

5. Verify the result before scheduling anything

  1. Run the whole script after tomorrow’s data is available. Expected: one notification inside Home Assistant, showing tomorrow’s date, a two-hour start/end window and an average EUR/kWh spot price. There is no phone push or appliance command.
  2. Compare the returned intervals. Open the script’s trace, the record of its actions and variables. Inspect prices and plan. Check that the selected block fits between 08:00 and 22:00. For equal-length slots, average their prices and divide by 1,000; the result should match the message after rounding.
  3. Run it again. Expected: the same notification is replaced, rather than a second copy appearing. This follows the documented notification ID behavior.
  4. Check the no-result path harmlessly. Temporarily change run_seconds to 90000 (25 hours), save and run again. Expected: “No plan,” since that duration cannot fit. Restore 7200, save and rerun. No household equipment is involved in either test.

For a daily preview, create an automation with a time trigger at 18:00 and an action that calls this saved script. Confirm that 18:00 in your Home Assistant time zone is after the relevant publication time. This is one daily attempt; it does not retry automatically or catch up after a missed trigger. If Home Assistant was off or prices were unavailable, run the script manually later. Always read the dated message.

If you decide to start an appliance automatically later, first check the exact model’s integration, remote-start permission and manufacturer instructions. Use supported delayed-start controls and a ready-to-run check. Turning a smart plug on does not establish that a dishwasher will start its program, and cutting power is not a suitable way to pause a wash between cheap intervals.

If the plan looks wrong

What you seeCheck firstExpected correction
“No price plan is ready” remainsThe trace at the Nord Pool action; tomorrow’s availability, internet access and config entry.A successful later fetch replaces the notice. Do not substitute zero prices.
“No plan” despite a successful fetchThe area key, date, allowed-window coverage and interval lengths in the response.Complete adjoining 15-minute/hourly data produces a candidate. Do not remove the gap check to force a result.
The time differs from the provider’s screenUTC versus local time, the date and Home Assistant’s configured time zone.Compare the same actual interval. Keep timestamp conversion; do not hard-code a UTC offset.
The price is 1,000 times too large or smallWhether the source is the action’s EUR/MWh response or an already converted sensor.Apply MWh-to-kWh conversion once. This code expects the action response.
The suggestion is cheap but the bill is notTime-varying fees, taxes, total consumption and the appliance’s uneven power use.Use the right combined rate and load profile before claiming actual savings.
No appliance startsThe purpose of this script.This is expected: it creates a plan only.

If the issue is reconciling recorded consumption and cost, the Energy dashboard versus utility bill guide covers that separate task. A price in EUR/kWh is a rate; it is not an energy reading in kWh.

Common questions

Does this work without the internet?

The comparison runs in Home Assistant, but it needs the online Nord Pool request to succeed. This example does not cache a future schedule for an outage. Local calculations do not make the source of prices local.

Can a negative spot price mean free electricity?

Not necessarily. The script accepts negative numeric prices, but network charges, taxes and supplier terms can leave a positive final cost. Check the contract before treating a negative market rate as a reason to use more energy.

Can I use this with a different supplier or outside Europe?

The comparison principle applies, but this script specifically expects the Nord Pool response format and EUR/MWh units. Use an integration that matches your own tariff; do not paste another provider’s data into it without checking fields, units, dates and charges.

For help planning everyday routines and their fallback behavior, see Tara’s home automation kit for coordinated local routines.