Cheapest Energy Hours - Jinja macro for dynamic energy prices

so if i push button at 00:15 it will take time btw 00:15 - 06:00 right?

Well, kinda, if you push the button at 00:15 it will take the times from then until 6:00 the next day, so that’s 23:45 hours on that day, and 6 hours of the next day. As the prices for the next day are not known at 00:15 it will use what it has, which is 00:15 - 06:00.

You probably want something like this (you didn’t provide the attributes with the prices for tomorrow, I assumed it’s prices_tomorrow):

{% from ‘cheapest_energy_hours.jinja’ import cheapest_energy_hours %}
{% set end = today_at('06:00') %}
{% set end = end + timedelta(days=1) if now() > end else end %}
{{ cheapest_energy_hours('sensor.energi_data_service_incl_vat_tarif', attr_today='prices_today'', attr_tomorrow='prices_tomorrow', hours=4, start=now(), end=end) }}

Hi there,

My goal is to create an average cheapest energy hours of 3 hours.

While reading the docs I can’t find out how and where to implement the tibber source sensor. Should it be in configuration.yaml or as my other sensors in the sensor directory? I have succesfully installed cheapest energy hours through hacs.

If you have them in a sensor folder, it sounds like you’re still using the legacy format.
This Tibber sensor uses the modern template sensor format. You can paste it directly in your configuration.yaml if you don’t use the template: integration in there already

My washing machine lets me manually set a start delay. As a result, I created this table to offer an overview of the upcoming peak solar energy times (which is even less expensive than the lowest electricity rates) and the cheapest electricity times for the next 12 and 24 hours. Using this table, you can manually set the delay on the machine.

It would be helpful to create a macro that performs a similar function but is specifically designed to focus on “highest solar hours.” For instance, it could calculate the time when we can expect 2000 W of solar power to run the washing machine entirely on solar energy. I have a sensor with data from forecast.solar, which includes attributes that provide the expected solar power for each hour. I created a sensor that calculates the info, but this is not as flexible as a macro like this one.

Screenshot 2025-01-26 14.06.56 - Display 2

<table border="1">
  <tr>
    <th>Source</th>
    <th>Period</th>
    <th>Start Time</th>
    <th>Time Until Start</th>
  </tr>
  <tr>
    <td>Sun</td>
    <td>Today</td>
    <td>
      {%- set start = states('sensor.power_highest_peak_time_today') -%}
      {%- set start_datetime = start | as_datetime -%}
      {%- if start_datetime -%}
        {%- set start_local = start_datetime.astimezone() -%}
        {%- set start_readable = start_local.strftime('%d-%m %H:%M') -%}
        {{ start_readable }}
      {%- else -%}
        Invalid time
      {%- endif -%}
    </td>
    <td>
      {%- if start_local -%}
        {%- if start_local > now() -%}
          {%- set time_until_seconds = (start_local - now()).total_seconds() -%}
          {%- set time_until_hours = (time_until_seconds / 3600) | round(2) -%}
          {{ time_until_hours }} hours
        {%- else -%}
          Time has already passed
        {%- endif -%}
      {%- else -%}
        Invalid time
      {%- endif -%}
    </td>
  </tr>
  <tr>
    <td>Sun</td>
    <td>Tomorrow</td>
    <td>
      {%- set start = states('sensor.power_highest_peak_time_tomorrow') -%}
      {%- set start_datetime = start | as_datetime -%}
      {%- if start_datetime -%}
        {%- set start_local = start_datetime.astimezone() -%}
        {%- set start_readable = start_local.strftime('%d-%m %H:%M') -%}
        {{ start_readable }}
      {%- else -%}
        Invalid time
      {%- endif -%}
    </td>
    <td>
      {%- if start_local -%}
        {%- if start_local > now() -%}
          {%- set time_until_seconds = (start_local - now()).total_seconds() -%}
          {%- set time_until_hours = (time_until_seconds / 3600) | round(2) -%}
          {{ time_until_hours }} hours
        {%- else -%}
          Time has already passed
        {%- endif -%}
      {%- else -%}
        Invalid time
      {%- endif -%}
    </td>
  </tr>
  <tr>
    <td>Cheapest electricity</td>
    <td>Next 12 hours</td>
    <td>
      {%- set sensor = 'sensor.average_electricity_price_today' -%}
      {%- set start = now() -%}
      {%- set end = start + timedelta(hours=12) -%}
      {%- from "cheapest_energy_hours.jinja" import cheapest_energy_hours -%}
      {%- set start_time = cheapest_energy_hours(sensor=sensor, hours=2, start=start, end=end) | as_datetime -%}
      {%- if start_time -%}
        {%- set start_local = start_time.astimezone() -%}
        {{ start_local.strftime('%d-%m %H:%M') }}
      {%- else -%}
        Invalid time
      {%- endif -%}
    </td>
    <td>
      {%- if start_local -%}
        {%- if start_local > now() -%}
          {%- set time_until_seconds = (start_local - now()).total_seconds() -%}
          {%- set time_until_hours = (time_until_seconds / 3600) | round(2) -%}
          {{ time_until_hours }} hours
        {%- else -%}
          Time has already passed
        {%- endif -%}
      {%- else -%}
        Invalid time
      {%- endif -%}
    </td>
  </tr>
  <tr>
    <td>Cheapest electricity</td>
    <td>Next 24 hours</td>
    <td>
      {%- set sensor = 'sensor.average_electricity_price_today' -%}
      {%- set start = now() -%}
      {%- set end = start + timedelta(hours=24) -%}
      {%- from "cheapest_energy_hours.jinja" import cheapest_energy_hours -%}
      {%- set start_time = cheapest_energy_hours(sensor=sensor, hours=2, start=start, end=end) | as_datetime -%}
      {%- if start_time -%}
        {%- set start_local = start_time.astimezone() -%}
        {{ start_local.strftime('%d-%m %H:%M') }}
      {%- else -%}
        Invalid time
      {%- endif -%}
    </td>
    <td>
      {%- if start_local -%}
        {%- if start_local > now() -%}
          {%- set time_until_seconds = (start_local - now()).total_seconds() -%}
          {%- set time_until_hours = (time_until_seconds / 3600) | round(2) -%}
          {{ time_until_hours }} hours
        {%- else -%}
          Time has already passed
        {%- endif -%}
      {%- else -%}
        Invalid time
      {%- endif -%}
    </td>
  </tr>
</table>

Is possible to get when energy prices procentage drop from the average is at the most (below a given threshold) ?

I dont know the maximum sequental hours, but the minimum of sequental hours that I need.(eg 4 hours)-

For example
If the lowest prices are between 22-05 => 7 hours
If the the lowest prices are between 01-05 => 4 hours

I want to charge the car at least 4 hours every night, but if the cheap period is longer I want to charge it more.

For the life of me i cannot figure this one out. And i dont know if this is the right place to ask.

I have dynamic energy prices that come from the energyzero integration.
what is try to achieve is as follows:

Every day at 00:00 so midnight i would like a sensor that tells me the following things, for the current day what are a the cheapest, moderate and expensive hours of the day.

So lets say when the energy prices are:

00:00 - 08:00 Cheapest
08:00 - 15:00 moderate
15:00 - 20:00 Expensive
20:00 - 00:00 moderate

the levels of cheapest moderate and expensive are predefined percentage of the range lowest and highest of that particular day.

what I would love to do is when the current time falls between set times e.g. 00:00 - 08:00 to activate my EV charger and that the charging stop (if fully charged or not) to stop at 08:00 and than continue again when another “Cheap” block comes along.

and to provide me with a notification on my phone to let me know a cheap hour block is starting in x amount of minutes.

I have attempted to build something like this even with the help of chatgpt, but no luck.

does anbody have an idea on how to something like this, or is willing to help me with this.

I know what I roughly need to do:

  • First get the prices including timestamp which are in the sensor.energy_zero_prices under attribute “prices”
  • then go trough the items in that list one by one saving values from first to the last on where the price is equal or below the threshold for cheapest.
  • then formating the first timestamp as xx:xx and same for the last timestamp
[
  {
    "timestamp": "2025-01-27 23:00:00+00:00",
    "price": 0.0616
  },
  {
    "timestamp": "2025-01-28 00:00:00+00:00",
    "price": 0.05573
  },
  {
    "timestamp": "2025-01-28 01:00:00+00:00",
    "price": 0.04295
  },
  {
    "timestamp": "2025-01-28 02:00:00+00:00",
    "price": 0.05138
  },
  {
    "timestamp": "2025-01-28 03:00:00+00:00",
    "price": 0.05777
  },
  {
    "timestamp": "2025-01-28 04:00:00+00:00",
    "price": 0.05707
  },
  {
    "timestamp": "2025-01-28 05:00:00+00:00",
    "price": 0.07782
  },
  {
    "timestamp": "2025-01-28 06:00:00+00:00",
    "price": 0.10355
  },
  {
    "timestamp": "2025-01-28 07:00:00+00:00",
    "price": 0.1276
  },
  {
    "timestamp": "2025-01-28 08:00:00+00:00",
    "price": 0.11747
  },
  {
    "timestamp": "2025-01-28 09:00:00+00:00",
    "price": 0.1002
  },
  {
    "timestamp": "2025-01-28 10:00:00+00:00",
    "price": 0.09804
  },
  {
    "timestamp": "2025-01-28 11:00:00+00:00",
    "price": 0.09962
  },
  {
    "timestamp": "2025-01-28 12:00:00+00:00",
    "price": 0.09831
  },
  {
    "timestamp": "2025-01-28 13:00:00+00:00",
    "price": 0.10122
  },
  {
    "timestamp": "2025-01-28 14:00:00+00:00",
    "price": 0.11393
  },
  {
    "timestamp": "2025-01-28 15:00:00+00:00",
    "price": 0.12246
  },
  {
    "timestamp": "2025-01-28 16:00:00+00:00",
    "price": 0.125
  },
  {
    "timestamp": "2025-01-28 17:00:00+00:00",
    "price": 0.1205
  },
  {
    "timestamp": "2025-01-28 18:00:00+00:00",
    "price": 0.11734
  },
  {
    "timestamp": "2025-01-28 19:00:00+00:00",
    "price": 0.08784
  },
  {
    "timestamp": "2025-01-28 20:00:00+00:00",
    "price": 0.07831
  },
  {
    "timestamp": "2025-01-28 21:00:00+00:00",
    "price": 0.069
  },
  {
    "timestamp": "2025-01-28 22:00:00+00:00",
    "price": 0.0555
  },
  {
    "timestamp": "2025-01-28 23:00:00+00:00",
    "price": 0.07579
  },
  {
    "timestamp": "2025-01-29 00:00:00+00:00",
    "price": 0.07191
  },
  {
    "timestamp": "2025-01-29 01:00:00+00:00",
    "price": 0.06574
  },
  {
    "timestamp": "2025-01-29 02:00:00+00:00",
    "price": 0.05994
  },
  {
    "timestamp": "2025-01-29 03:00:00+00:00",
    "price": 0.06401
  },
  {
    "timestamp": "2025-01-29 04:00:00+00:00",
    "price": 0.07023
  },
  {
    "timestamp": "2025-01-29 05:00:00+00:00",
    "price": 0.08116
  },
  {
    "timestamp": "2025-01-29 06:00:00+00:00",
    "price": 0.12695
  },
  {
    "timestamp": "2025-01-29 07:00:00+00:00",
    "price": 0.1362
  },
  {
    "timestamp": "2025-01-29 08:00:00+00:00",
    "price": 0.13045
  },
  {
    "timestamp": "2025-01-29 09:00:00+00:00",
    "price": 0.10505
  },
  {
    "timestamp": "2025-01-29 10:00:00+00:00",
    "price": 0.08925
  },
  {
    "timestamp": "2025-01-29 11:00:00+00:00",
    "price": 0.085
  },
  {
    "timestamp": "2025-01-29 12:00:00+00:00",
    "price": 0.08553
  },
  {
    "timestamp": "2025-01-29 13:00:00+00:00",
    "price": 0.08767
  },
  {
    "timestamp": "2025-01-29 14:00:00+00:00",
    "price": 0.10275
  },
  {
    "timestamp": "2025-01-29 15:00:00+00:00",
    "price": 0.13272
  },
  {
    "timestamp": "2025-01-29 16:00:00+00:00",
    "price": 0.14957
  },
  {
    "timestamp": "2025-01-29 17:00:00+00:00",
    "price": 0.165
  },
  {
    "timestamp": "2025-01-29 18:00:00+00:00",
    "price": 0.1486
  },
  {
    "timestamp": "2025-01-29 19:00:00+00:00",
    "price": 0.1353
  },
  {
    "timestamp": "2025-01-29 20:00:00+00:00",
    "price": 0.1221
  },
  {
    "timestamp": "2025-01-29 21:00:00+00:00",
    "price": 0.12235
  },
  {
    "timestamp": "2025-01-29 22:00:00+00:00",
    "price": 0.1001
  },
  {
    "timestamp": "2025-01-29 23:00:00+00:00",
    "price": 0.13134
  },
  {
    "timestamp": "2025-01-30 00:00:00+00:00",
    "price": 0.12579
  },
  {
    "timestamp": "2025-01-30 01:00:00+00:00",
    "price": 0.1119
  },
  {
    "timestamp": "2025-01-30 02:00:00+00:00",
    "price": 0.10448
  },
  {
    "timestamp": "2025-01-30 03:00:00+00:00",
    "price": 0.1
  },
  {
    "timestamp": "2025-01-30 04:00:00+00:00",
    "price": 0.10109
  },
  {
    "timestamp": "2025-01-30 05:00:00+00:00",
    "price": 0.13136
  },
  {
    "timestamp": "2025-01-30 06:00:00+00:00",
    "price": 0.14965
  },
  {
    "timestamp": "2025-01-30 07:00:00+00:00",
    "price": 0.1603
  },
  {
    "timestamp": "2025-01-30 08:00:00+00:00",
    "price": 0.15149
  },
  {
    "timestamp": "2025-01-30 09:00:00+00:00",
    "price": 0.1349
  },
  {
    "timestamp": "2025-01-30 10:00:00+00:00",
    "price": 0.12325
  },
  {
    "timestamp": "2025-01-30 11:00:00+00:00",
    "price": 0.11574
  },
  {
    "timestamp": "2025-01-30 12:00:00+00:00",
    "price": 0.1133
  },
  {
    "timestamp": "2025-01-30 13:00:00+00:00",
    "price": 0.12574
  },
  {
    "timestamp": "2025-01-30 14:00:00+00:00",
    "price": 0.14267
  },
  {
    "timestamp": "2025-01-30 15:00:00+00:00",
    "price": 0.15105
  },
  {
    "timestamp": "2025-01-30 16:00:00+00:00",
    "price": 0.17809
  },
  {
    "timestamp": "2025-01-30 17:00:00+00:00",
    "price": 0.17993
  },
  {
    "timestamp": "2025-01-30 18:00:00+00:00",
    "price": 0.16124
  },
  {
    "timestamp": "2025-01-30 19:00:00+00:00",
    "price": 0.14849
  },
  {
    "timestamp": "2025-01-30 20:00:00+00:00",
    "price": 0.13259
  },
  {
    "timestamp": "2025-01-30 21:00:00+00:00",
    "price": 0.12573
  },
  {
    "timestamp": "2025-01-30 22:00:00+00:00",
    "price": 0.1202
  }
]

for example when threshold is 0.06 and below then the values that need to processed are

 {
    "timestamp": "2025-01-28 00:00:00+00:00",
    "price": 0.05573
  },
  {
    "timestamp": "2025-01-28 01:00:00+00:00",
    "price": 0.04295
  },
  {
    "timestamp": "2025-01-28 02:00:00+00:00",
    "price": 0.05138
  },
  {
    "timestamp": "2025-01-28 03:00:00+00:00",
    "price": 0.05777
  },
  {
    "timestamp": "2025-01-28 04:00:00+00:00",
    "price": 0.05707
  },

which would eventually result in sensor output: 00:00 - 04:00

which would be the first block/window of hours to considered to be cheapest

Hope this gives some idea with what i would like to achieve.

Kinds regards,
Jordy

You can get most of this data using my macro.

Use the macro without start and end time to get the average price of the day.
Then use it to get the average prices of your time blocks. You can compare those to the day average to get your price inications.

"Is it possible to use Calendar Integration to indicate when I need my car again? Ideally, the macro would then distribute the charging over the cheapest hours on those days.

You can create an automation which creates the calendar entries, based on the result of the macro.

Hi TheFes thank your great work with this macro.
I’ve been using your macro from some days without any problem, however today, after the change from Daylight Savings I’ve got the ZeroDivisionError, which was identified last year. Is there anything that I can do to avoid this error or do I still have to wait until tomorrow…

That was in October, when DST ended. Could be that I accidentally moved the problem to the start of DST.

What is the template you are using that caused the problem?

I’m using Omie. Today it is working again as I expetected.

Hi there,

My energy provider is tibber and I use the source sensor as suggeted in docs.

While running the action Tibber: Get energy prices in developer tools, the response is as expected:

prices:
  REDACTED -:
    - start_time: "2025-04-14T00:00:00.000+02:00"
      price: 0.2464
      level: NORMAL
    - start_time: "2025-04-14T01:00:00.000+02:00"
      price: 0.242
      level: NORMAL
    - start_time: "2025-04-14T02:00:00.000+02:00"
      price: 0.2415
      level: NORMAL
    - start_time: "2025-04-14T03:00:00.000+02:00"
      price: 0.2415
      level: NORMAL
    - start_time: "2025-04-14T04:00:00.000+02:00"
      price: 0.2436
      level: NORMAL
    - start_time: "2025-04-14T05:00:00.000+02:00"
      price: 0.255
      level: HIGH
    - start_time: "2025-04-14T06:00:00.000+02:00"
      price: 0.2863
      level: HIGH
    - start_time: "2025-04-14T07:00:00.000+02:00"
      price: 0.3298
      level: HIGH
    - start_time: "2025-04-14T08:00:00.000+02:00"
      price: 0.3215
      level: HIGH
    - start_time: "2025-04-14T09:00:00.000+02:00"
      price: 0.2687
      level: HIGH
    - start_time: "2025-04-14T10:00:00.000+02:00"
      price: 0.2506
      level: HIGH
    - start_time: "2025-04-14T11:00:00.000+02:00"
      price: 0.2324
      level: NORMAL
    - start_time: "2025-04-14T12:00:00.000+02:00"
      price: 0.2213
      level: LOW
    - start_time: "2025-04-14T13:00:00.000+02:00"
      price: 0.2161
      level: LOW
    - start_time: "2025-04-14T14:00:00.000+02:00"
      price: 0.2161
      level: LOW
    - start_time: "2025-04-14T15:00:00.000+02:00"
      price: 0.2281
      level: LOW
    - start_time: "2025-04-14T16:00:00.000+02:00"
      price: 0.2403
      level: NORMAL
    - start_time: "2025-04-14T17:00:00.000+02:00"
      price: 0.2552
      level: HIGH
    - start_time: "2025-04-14T18:00:00.000+02:00"
      price: 0.2787
      level: HIGH
    - start_time: "2025-04-14T19:00:00.000+02:00"
      price: 0.3655
      level: HIGH
    - start_time: "2025-04-14T20:00:00.000+02:00"
      price: 0.3275
      level: HIGH
    - start_time: "2025-04-14T21:00:00.000+02:00"
      price: 0.266
      level: HIGH
    - start_time: "2025-04-14T22:00:00.000+02:00"
      price: 0.2417
      level: NORMAL
    - start_time: "2025-04-14T23:00:00.000+02:00"
      price: 0.2396
      level: NORMAL

but im not able fetch data no valid sensor found.


What am i overseeing?

tibber.get_prices is an action, not an entity. You need to create a trigger based template sensor as described in the documentation which you can the use as source of the macro.

I have found in my config (sensors.yaml) a rest integration

  • platform: rest
    unique_id: energyzero_elektra_prijzen
    name: Stroomprijs per uur
    resource: https://api.energyzero.nl/v1/energyprices
    scan_interval: 900
    unit_of_measurement: “EUR/kWh”
    value_template: >
    {{value_json.Prices[now().hour].price}}
    params:
    fromDate: >
    {{(now().strftime(‘%Y-%m-%d’)|as_datetime).isoformat()}}Z
    tillDate: >
    {{((now()+ timedelta(days=2)).strftime(‘%Y-%m-%d’)|as_datetime).isoformat()}}Z
    interval: 4
    usageType: 1
    inclBtw: true
    json_attributes:
    • Prices
    • readingDate

So i have the sensor, but when i run this in the template editor, i get this result

@rscheurw1973
It’s time24, without the whitespace

Something more advanced. I’m trying to make the weight points work. Using it to find the cheapest charge times for my home battery because the charge pattern is not linear. A full charge (most common, takes 2,75 hours)

First 90 minutes at ~7.5 kw
Then 15 minutes at ~ 3.1 kw
Then 60 minutes at ~ 1.5

With weight_point = 4 I assume each point corresponds with 15 minutes.
So 6x 7.5, then 1x 3.1 and 4x 1.5 → total points (2 hour + 3x15 minutes = 2.75 hour), right?

{% from 'cheapest_energy_hours.jinja' import cheapest_energy_hours %}
{% set output_json = cheapest_energy_hours
  (sensor='sensor.average_electricity_price',
   attr_today='prices_today', 
   attr_tomorrow='prices_tomorrow', 
   lowest=true,
   split=true,
   no_weight_points=4,
   weight=[7.5,7.5,7.5,7.5, 7.5,7.5, 3.1, 1.5, 1.5, 1.5, 1.5],
   time_key='time', 
   value_key='price', 
   hours=duration, 
   start=now(), 
   include_tomorrow=true,
   mode='all') %}
{{ output_json }}

It works but the result is not what I expected.

[{“start”:“2025-05-31T12:00:00+02:00”,“end”:“2025-05-31T12:30:00+02:00”,“hours”:0.5,“prices”:[-0.00307,-0.00307],“is_now”:false},{“start”:“2025-05-31T12:45:00+02:00”,“end”:“2025-05-31T15:00:00+02:00”,“hours”:2.25,“prices”:[-0.00307,-0.01317,-0.01317,-0.01317,-0.01317,-0.00775,-0.00775,-0.00775,-0.00775],“is_now”:false},{“total_hours”:2.75,“datapoints_per_hour”:4.0,“datapoints”:11}]

Price 12:00 = -0.00307
Price 13:00 = -0.01317 ← LOWEST
Price 14:00 = -0.00775

It’s now scheduling 30+15 minutes between 12:00-13:00
But this means that in the lowest hour only 3 of the highest 7.5 sections remain, + 1x 3.1.

I was expecting that this macro would align 4 of the 7.5 pieces on the lowest hour. Am I missing something?

Without knowing the other prices around that block it’s not possible to determine if it took it right slot.
If the prices at 15:00 are for example above 0, it would negatively affect the total price for the entire 3 hours.

Wait, now I see what you mean… I missed you were using split

[
   {
      "start":"2025-05-31T12:00:00+02:00",
      "end":"2025-05-31T12:30:00+02:00",
      "hours":0.5,
      "prices":[
         -0.00307,
         -0.00307
      ],
      "is_now":false
   },
   {
      "start":"2025-05-31T12:45:00+02:00",
      "end":"2025-05-31T15:00:00+02:00",
      "hours":2.25,
      "prices":[
         -0.00307,
         -0.01317,
         -0.01317,
         -0.01317,
         -0.01317,
         -0.00775,
         -0.00775,
         -0.00775,
         -0.00775
      ],
      "is_now":false
   },
   {
      "total_hours":2.75,
      "datapoints_per_hour":4.0,
      "datapoints":11
   }
]

This is the output of the macro.
The 2.75 hours is split in 11 parts of 15 minutes.
The complete cheapest hour (13:00) mentioned by you use in the second part.

As the price at 14:00 is cheaper than the 12:00 price, 14:00 is also included fully.
The only thing which it did a bit weird is to start at 12:00, and not at 12:15. If it would have started at 12:15 it would be one full block