TheFes / cheapest-energy-hours

Jinja macro to find the cheapest energy prices
GNU General Public License v3.0
85 stars 9 forks source link

Unable to keep binary_sensor switched on #114

Closed c0mplex1 closed 9 months ago

c0mplex1 commented 9 months ago

​I'm stuck. What I want is a binary sensor that turns on, and stays on for the next hour(s), when the electricity price is lowest. If the price goes up, the sensor must switch off, but if the price falls again, the switch must switch on again. I want this for 2 periods (day/night): from 08:00 to 20:00 and from 20:00 to 08:00. No matter what I try, I can't get it to work. I get the idea that what I want is not possible.

For example

Time Rate Sensor
20:00 0.11 off
21:00 0.11 off
22:00 0.10 off
23:00 0.08 ON
00:00 0.09 off
01:00 0.08 ON
02:00 0.08 ON
03:00 0.09 off
04:00 0.08 ON
05:00 0.08 ON
06:00 0.08 ON
07:00 0.10 off

The automations I tried are as follows:

template:
  binary_sensor:
    - unique_id: cheap_hour_day
      name: Cheap Hour (Day)
      state: >
        {% set start = today_at("08:00") %}
        {% set sensor = "sensor.electricity_price" %}
        {% from "cheapest_energy_hours.jinja" import cheapest_energy_hours %}
        {% set n = now() %}
        {{ cheapest_energy_hours (
             sensor = sensor, attr_all = "Prices", time_key = "readingDate",
             start = start, end = start + timedelta(hours=12), mode = "is_now" )
        }}

template:
  - trigger:
      - platform: time_pattern
        # Trigger once per hour
        hours: "*"
    binary_sensor:
      - unique_id: cheap_hour_day
        name: Cheap Hour (Day)
        state: >
          {% set start = today_at('08:00') %}
          {% set end = (start +  timedelta(hours=12)) %}
          {% set sensor = "sensor.electricity_price" %}
          {% from "cheapest_energy_hours.jinja" import cheapest_energy_hours %}
          {{ cheapest_energy_hours (
               sensor = sensor, attr_all = "Prices", time_key = "readingDate",
               hours = 12, start = start, end = end, mode = "is_now" )
          }}
      - unique_id: cheap_hour_night
        name: Cheap Hour (Night)
        state: >
          {% set start = today_at("20:00") %}
          {% set end = (start +  timedelta(hours=12)) %}
          {% set sensor = "sensor.electricity_price" %}
          {% from "cheapest_energy_hours.jinja" import cheapest_energy_hours %}
          {{ cheapest_energy_hours (
               sensor = sensor, attr_all = "Prices", time_key = "readingDate",
               hours = 12, start = start, end = end, mode = "is_now" )
          }}
  - binary_sensor:
      - name: Test nacht
        unique_id: test_nacht
        state: >
          {% set day = timedelta(days=1) %}
          {% set start = today_at("20:00") %}
          {% set end = today_at("08:00") %}
          {% set after_mn = now() < end %}
          {% set start = start-day if after_mn else start %}
          {% set end = end if after_mn else end+day %}
          {% set sensor = "sensor.electricity_price" %}
          {% from "cheapest_energy_hours.jinja" import cheapest_energy_hours %}
          {{ cheapest_energy_hours (
               sensor = sensor, attr_all = "Prices", time_key = "readingDate",
               hours = 1, start = start, end = end, look_ahead = true, mode = "is_now" )
          }}
###################################################################
        {% set start = today_at("08:00") %}
        {% set end = today_at("20:00") %}
        {% from 'cheapest_energy_hours.jinja' import cheapest_energy_hours %}
        {% set sensor = "sensor.electricity_price" %}
        {{ cheapest_energy_hours (
             sensor = sensor, attr_all = "Prices", time_key = "readingDate",
             start = start, end = start + timedelta(hours=12), mode = "is_now" ) }}
###################################################################
        {% from 'cheapest_energy_hours.jinja' import cheapest_energy_hours %}
        {% set sensor = "sensor.electricity_price" %}
        {{ cheapest_energy_hours (
             sensor = sensor, attr_all = "Prices", time_key = "readingDate",
             start = "20:00", end = "08:00", include_tomorrow = true, mode = "is_now" ) }}
###################################################################
        {% from 'cheapest_energy_hours.jinja' import cheapest_energy_hours %}
        {% set sensor = "sensor.electricity_price" %}
        {% set end = (now() +  timedelta(hours=12)).strftime('%H:00') %}
        {% set tomorrow = now() > today_at(end) %}
        {{ cheapest_energy_hours (
             sensor = sensor, attr_all = "Prices", time_key = "readingDate",
             hours = 1, end = end, look_ahead = true, include_tomorrow = tomorrow, mode = "is_now" ) }}
TheFes commented 9 months ago

What you want is surely possible, but the macro is not built for this

Some remarks:

I could probably build in support for this, but it's a rather simple only line template without the need for the macro.

For 8:00 - 20:00

{% set sensor = "sensor.electricity_price" %}
{% set within_time = today_at('08:00') < now() < today_at('20:00') %}
{% if sensor | has_value and within_time %}
  {% set current_price = states(sensor) | float %}
  {% set cheapest_price = state_attr(sensor, 'Prices')[8:19] | map(attribute='price') | min %}
  {{ current_price == cheapest_price }}
{% else %}
  {{ False }}
{% endif %}

20:00 - 08:00 is a lot more tricky though, I don't think that it is possible to do that 100% reliably in a template binary sensor only. The thing is that the lowest price can potentially be at 22:00 and you will lose that price after midnight. Unless your sensor also shows yesterday's prices (which will make the template above incorrect as well)

TheFes commented 9 months ago

I've added a mode extreme_now to do what you requested. However, you will still have issues when the day changes at midnight, and you lose the data of the previous day.

I'm waiting for some other things, but you can manually update to the version on the main branch.

c0mplex1 commented 9 months ago

At the moment I test the solution below, based on your replay a few days ago. Because my programming skills are not that high, I have to test it in real-time.

template:
  # Get the lowest price between 8:00 - 20:00 (Day) and between 20:00 - 8:00 (Night).
  - trigger:
      - platform: time
        at: "16:10"
    sensor:
      - unique_id: electricity_night_min_price
        name: Electricity Night Min Price
        state: >
          {{ state_attr("sensor.electricity_price", "Prices")[20:32] | map(attribute="price") | min }}
      - unique_id: electricity_day_min_price
        name: Electricity Day Min Price
        state: >
          {{ state_attr("sensor.electricity_price", "Prices")[32:44] | map(attribute="price") | min }}

  - binary_sensor:
      - name: Test dag/nacht
        unique_id: test_dag_nacht
        state: >
          {% set sensor = "sensor.electricity_price" %}
          {% set is_day = today_at("08:00") <= now() < today_at("20:00") %}
          {% if sensor | has_value %}
            {% set current_price = states(sensor) | float(0) %}
            {% if is_day %}
              {% set cheapest_price = states("sensor.electricity_day_min_price") | float(0) %}
            {% else %}
              {% set cheapest_price = states("sensor.electricity_night_min_price") | float(0) %}
            {% endif %}
            {{ current_price == cheapest_price }}
          {% else %}
            False
          {% endif %}
TheFes commented 9 months ago

It looks like you also have the data of yesterday in your sensor (as you are taking 20:44 for 8:00 - 20:00 In that case you can work with the data the sensor provides.

With the setup above, you will be using yesterdays data until 16:10, because then the two trigger based sensors will refresh.

Can you show what is currently in the forecast of your electricity sensor?

c0mplex1 commented 9 months ago

This is what I currently have:

Prices:
  - price: 0.07
    readingDate: "2024-02-14T23:00:00Z"
  - price: 0.07
    readingDate: "2024-02-15T00:00:00Z"
  - price: 0.07
    readingDate: "2024-02-15T01:00:00Z"
  - price: 0.07
    readingDate: "2024-02-15T02:00:00Z"
  - price: 0.07
    readingDate: "2024-02-15T03:00:00Z"
  - price: 0.07
    readingDate: "2024-02-15T04:00:00Z"
  - price: 0.08
    readingDate: "2024-02-15T05:00:00Z"
  - price: 0.1
    readingDate: "2024-02-15T06:00:00Z"
  - price: 0.1
    readingDate: "2024-02-15T07:00:00Z"
  - price: 0.1
    readingDate: "2024-02-15T08:00:00Z"
  - price: 0.08
    readingDate: "2024-02-15T09:00:00Z"
  - price: 0.08
    readingDate: "2024-02-15T10:00:00Z"
  - price: 0.07
    readingDate: "2024-02-15T11:00:00Z"
  - price: 0.07
    readingDate: "2024-02-15T12:00:00Z"
  - price: 0.08
    readingDate: "2024-02-15T13:00:00Z"
  - price: 0.08
    readingDate: "2024-02-15T14:00:00Z"
  - price: 0.08
    readingDate: "2024-02-15T15:00:00Z"
  - price: 0.1
    readingDate: "2024-02-15T16:00:00Z"
  - price: 0.1
    readingDate: "2024-02-15T17:00:00Z"
  - price: 0.1
    readingDate: "2024-02-15T18:00:00Z"
  - price: 0.08
    readingDate: "2024-02-15T19:00:00Z"
  - price: 0.07
    readingDate: "2024-02-15T20:00:00Z"
  - price: 0.07
    readingDate: "2024-02-15T21:00:00Z"
  - price: 0.07
    readingDate: "2024-02-15T22:00:00Z"
average: 0.08
unit_of_measurement: EUR/kWh
friendly_name: Electricity price

Between 13:00 and 15:00 I get the prices of tomorrow. Then I can use below, that's why I trigger at 16:10 {{ state_attr("sensor.electricity_price", "Prices")[32:44] | map(attribute="price") | min }}

TheFes commented 9 months ago

Yes, but you refresh the sensors at 16:10 that means that as of then you are using the data for tomorrow for the day price, while you should still use the data for today until 20:00

c0mplex1 commented 9 months ago

That's correct. When I refresh the sensor at 16:10 then the data of tomorrow is saved, but will be used between 08:00 and 20:00 next day.

TheFes commented 9 months ago

at 16:10 you will do this:

      - unique_id: electricity_day_min_price
        name: Electricity Day Min Price
        state: >
          {{ state_attr("sensor.electricity_price", "Prices")[32:44] | map(attribute="price") | min }}`

That will give you the prices from 8:00 - 20:00 tomorrow.

But at that time it is still before 20:00 today. So your binary sensor will check the current price against the cheapest day price tomorrow.

You should refresh the prices at 20:00

c0mplex1 commented 9 months ago

It's now 12:00 and the switch goes from false to true.

TheFes commented 9 months ago

It will use today's prices until 16:10. After that it will use tomorrows prices. The binary sensor will be incorrect between 16:10 and 20:00

c0mplex1 commented 9 months ago

BTW: I do not use prices of the next day. I only want the lowest price between 08:00 and 20:00 and then use it next day. At 16:00 the prices of next are available.

c0mplex1 commented 9 months ago

Let's wait a week or so to see how it works.

TheFes commented 9 months ago

Look, you have a trigger based template sensor, which triggers at 16:10. It will refresh the sensors defined under it at that time.

So at 16:10, these 2 sensors will get new values. This means, that this sensor will start showing the lowest price of 8:00 - 20:00 tomorrow.

But... Your binary sensor will compare the current price against that sensor, so from the period from 16:10 to 20:00 it will check if the current price is the same as the cheapest price between 8:00 and 20:00 tomorrow, not the cheapest price between 8:00 and 20:00 today.

That's why I'm saying you should use 20:00 for the trigger, not 16:10.

c0mplex1 commented 9 months ago

Aaaaaah, check. Now I see. I have changed the trigger time to 20:00.

If you want, you may close this topic.

I appreciate your help, and thank you very much for your explanation and patience.

TheFes commented 9 months ago

Okay, I have an alternative approach for you, using the latest version of the macro (not released yet).

First, I noticed that the prices including VAT coming from EnergyZero are rounded to 2 decimals, while the prices excluding VAT are accurate up to 5 decimals. So if you would use the prices excluding VAT, it would not show the same values multiple times per day, and you could actually determine the real cheapest moment.

In the example below I have used prices including VAT, to replicate your current sensor. The price sensor will update every hour, and will display the current price as it's state and has the prices of yesterday, today and tomorrow (if available) in the prices attribute. The advantage of also including yesterdays prices is, that you don't need to store the prices of the period between 20:00 and 8:00 in a separate template sensor, you can always access them.

template:
  # get the prices using the service call provided by the core Energy Zero integration
  - trigger:
      - platform: time_pattern
        hours: "/1"
      - platform: homeassistant
        event: start
    action:
      - service: energyzero.get_energy_prices
        data:
          incl_vat: true
          config_entry: fe7bdc80dd3bc850138998d869f1f19d
          start: "{{ today_at() - timedelta(days=1) }}"
          end: "{{ today_at() + timedelta(days=2) }}"
        response_variable: prices
    sensor:
      - unique_id: 79c470d8-4ccd-4f44-b3a2-e3d59d5dda8a
        name: Energy Zero prices
        state: "{{ prices.prices | selectattr('timestamp', '<=', utcnow().strftime('%Y-%m-%d %H:%M:%S+00:00')) | map(attribute='price') | list | last }}"
        attributes:
          prices: "{{ prices.prices }}"

  # binary sensor which will be on during the periods with the lowest price
  - binary_sensor:
      - unique_id: 5f2706e7-ccd2-4b74-ab8b-4dbb77209c1f
        name: Laagste prijs dag/nacht
        state: >
          {% from 'cheapest_energy_hours.jinja' import cheapest_energy_hours %}
          {%- set start_time, end_time = '08:00', '20:00' -%}
          {%- set day = today_at(start_time) <= now() < today_at(end_time) %}
          {%- set after_mn = now() < today_at(start_time) -%} 
          {%- set start=today_at(start_time) if day else (today_at(end_time) - timedelta(days=1 if after_mn else 0)) -%}
          {%- set end=today_at(end_time) if day else (today_at(start_time) + timedelta(days=0 if after_mn else 1)) -%}
          {{ cheapest_energy_hours('sensor.energy_zero_prices', time_key='timestamp', start=start, end=end, mode='extreme_now') }}
TheFes commented 9 months ago

Closing this, as you seem to have solved it yourself, and I provided another approach to tackle it as well.