Nordpool (ha integration) with 15min data. apexcharts and more. Updated

I’ve been trying to get Nordpool 15min data into HA and this is what i’ve managed so far.

Use the HA Core Nordopool integration that supports 15-min data (uninstall your hacs version from hacs first to be able to install this from devices)
Use a template to create graph data with corrections for vat and transfer costs
Use AIO energy management to automate cheap hours.

pasting code below

3 Likes

Guide:

  • first install nordpool integration in HA (not the HACS version) and set up your area and such.

  • add two input number helpers (you want to have enabled “advanced mode”
    under your profile pic in the lower left corner to be able to set stepsize for cost to 0.0001)
    image

  • input_number.electricity_tax

  • input_number.electricity_purchase_costs

i use 25% for tax and 0.7625 SEK/kWh for cost

  • And This template below is used in configuration.yaml

Config Entry is from the nordpool integration (to find it go to development tools, actions, do a nordpool.get_price request and go to yaml mode to find the number)

otherwise modify script to match your areas and such

  • Add a graph (code below) basic or advanced.

  • Add AIO energy management via hacs (code below) if you want to automate based on the price

1 Like

Energy Price Automation

To automate stuff based on 15-min data i’ve used aio_energy_management

install via HACS. add custom repository. past the github link

in configuration.yaml add something like this (change the config entry to your nordpool config entry)

this gives an binary sensor my_cheapest_hour that can be used in automations to turn on and off stuff based on cheapest times

aio_energy_management:
  cheapest_hours:
    - nordpool_official_config_entry: 01K6AEXZV4HDRNYVJQEJB2R5GT
      unique_id: my_cheapest_hours
      name: My Cheapest Hours
      mtu: 15
      first_hour: 19
      last_hour: 18
      starting_today: true
      number_of_hours: 8
      sequential: false
      failsafe_starting_hour: 22

tip:
it’s also possible to put an input_number helper as the number_of_hours input.

example with a input number helper named “my_cheapest_hours_setting”

      number_of_hours: input_number.my_cheapest_hours_setting

Template for data retreival 15 min interval
and basic apexcharts graph

template goes into configuration.yaml

edit the config_entry: to your nordpool entity and other parameters to math your place

template:
  - trigger:
      - trigger: time_pattern
        minutes: /15
      - trigger: homeassistant
        event: start
    action:
      - variables:
          area: "SE3"
          currency: "SEK"
          country: "Sweden"
          config_entry: "01K6AEXZV4HDRNYVJQEJB2R5GT"
          price_sensor: "sensor.nord_pool_se3_current_price"
      - action: nordpool.get_price_indices_for_date
        data:
          config_entry: "{{ config_entry }}"
          date: "{{ now().date() }}"
          areas: "{{ area }}"
          currency: "{{ currency }}"
          resolution: "15"
        response_variable: today_price
    sensor:
      - name: Electricity Prices Today
        unique_id: se3_electricity_prices_today
        unit_of_measurement: "SEK/kWh"
        icon: mdi:cash-clock
        state: >
          {% set current_price = (states(price_sensor) | float(0) * (1 + states('input_number.electricity_tax') | float(0)/100) + states('input_number.electricity_purchase_costs') | float(0)) | round(2) %}
          {{ current_price }}

        attributes:
          country: "{{ country }}"
          region: "{{ area }}"
          currency: "{{ currency }}"
          min: >
            {% set entries = today_price[area] %}
            {% if entries %}
              {% set min_val = entries | map(attribute='price') | min | float(0) / 1000 %}
              {{ (min_val * (1 + states('input_number.electricity_tax') | float(0)/100)
                + states('input_number.electricity_purchase_costs') | float(0)) | round(2) }}
            {% else %} unavailable {% endif %}
          max: >
            {% set entries = today_price[area] %}
            {% if entries %}
              {% set max_val = entries | map(attribute='price') | max | float(0) / 1000 %}
              {{ (max_val * (1 + states('input_number.electricity_tax') | float(0)/100)
                + states('input_number.electricity_purchase_costs') | float(0)) | round(2) }}
            {% else %} unavailable {% endif %}
          mean: >
            {% set entries = today_price[area] %}
            {% if entries %}
              {% set avg_val = entries | map(attribute='price') | map('float') | sum / (entries | count) / 1000 %}
              {{ (avg_val * (1 + states('input_number.electricity_tax') | float(0)/100)
                + states('input_number.electricity_purchase_costs') | float(0)) | round(2) }}
            {% else %} unavailable {% endif %}
          data: >
            {% set electricity_tax = states('input_number.electricity_tax') | float(0) %}
            {% set purchase_costs = states('input_number.electricity_purchase_costs') | float(0) %}
            {% set data = namespace(prices=[]) %}
            {% if today_price[area] | count > 0 %}
              {% for state in today_price[area] %}
                {% set corrected_start = as_datetime(state.start).astimezone().isoformat() %}
                {% set corrected_end = as_datetime(state.end).astimezone().isoformat() %}
                {% set val = (state.price/1000 * (1 + electricity_tax/100) + purchase_costs) | round(3, default=0) %}
                {% set data.prices = data.prices + [{'start': corrected_start, 'end': corrected_end, 'value': val}] %}
              {% endfor %}
            {% endif %}
            {{ data.prices }}

  - trigger:
      - trigger: time_pattern
        minutes: "10"
      - trigger: homeassistant
        event: start
    action:
      - variables:
          area: "SE3"
          currency: "SEK"
          country: "Sweden"
          config_entry: "01K6AEXZV4HDRNYVJQEJB2R5GT"
      - action: nordpool.get_price_indices_for_date
        data:
          config_entry: "{{ config_entry }}"
          date: "{{ now().date() + timedelta(days=1) }}"
          areas: "{{ area }}"
          currency: "{{ currency }}"
          resolution: "15"
        response_variable: tomorrow_price
    sensor:
      - name: Electricity Prices Tomorrow
        unique_id: se3_electricity_prices_tomorrow
        unit_of_measurement: "SEK/kWh"
        icon: mdi:cash-clock
        state: >
          {% set entries = tomorrow_price[area] %}
          {% if entries %}
            {% set avg_val = entries | map(attribute='price') | map('float') | sum / (entries | count) / 1000 %}
            {{ (avg_val * (1 + states('input_number.electricity_tax') | float(0)/100)
              + states('input_number.electricity_purchase_costs') | float(0)) | round(2) }}
          {% else %} unavailable {% endif %}
        attributes:
          country: "{{ country }}"
          region: "{{ area }}"
          currency: "{{ currency }}"
          tomorrow_valid: >
            {{ tomorrow_price.get(area) is defined and tomorrow_price[area] | count > 0 }}
          min: >
            {% set entries = tomorrow_price[area] %}
            {% if entries %}
              {% set min_val = entries | map(attribute='price') | min | float(0) / 1000 %}
              {{ (min_val * (1 + states('input_number.electricity_tax') | float(0)/100)
                + states('input_number.electricity_purchase_costs') | float(0)) | round(2) }}
            {% else %} unavailable {% endif %}
          max: >
            {% set entries = tomorrow_price[area] %}
            {% if entries %}
              {% set max_val = entries | map(attribute='price') | max | float(0) / 1000 %}
              {{ (max_val * (1 + states('input_number.electricity_tax') | float(0)/100)
                + states('input_number.electricity_purchase_costs') | float(0)) | round(2) }}
            {% else %} unavailable {% endif %}
          mean: >
            {% set entries = tomorrow_price[area] %}
            {% if entries %}
              {% set avg_val = entries | map(attribute='price') | map('float') | sum / (entries | count) / 1000 %}
              {{ (avg_val * (1 + states('input_number.electricity_tax') | float(0)/100)
                + states('input_number.electricity_purchase_costs') | float(0)) | round(2) }}
            {% else %} unavailable {% endif %}
          data: >
            {% set electricity_tax = states('input_number.electricity_tax') | float(0) %}
            {% set purchase_costs = states('input_number.electricity_purchase_costs') | float(0) %}
            {% set data = namespace(prices=[]) %}
            {% if tomorrow_price[area] | count > 0 %}
              {% for state in tomorrow_price[area] %}
                {% set corrected_start = as_datetime(state.start).astimezone().isoformat() %}
                {% set corrected_end = as_datetime(state.end).astimezone().isoformat() %}
                {% set val = (state.price/1000 * (1 + electricity_tax/100) + purchase_costs) | round(3, default=0) %}
                {% set data.prices = data.prices + [{'start': corrected_start, 'end': corrected_end, 'value': val}] %}
              {% endfor %}
            {% endif %}
            {{ data.prices }}

and here is an updated (again) graph to show current, average, min and max

type: custom:apexcharts-card
experimental:
  color_threshold: true
apex_config:
  responsive:
    - breakpoint: 500
      options:
        chart:
          height: 300px
    - breakpoint: 1200
      options:
        chart:
          height: 640px
    - breakpoint: 3000
      options:
        chart:
          height: 800px
  legend:
    show: false
  title:
    floating: false
    align: center
    style:
      fontSize: 20px
      fontWeight: bold
  xaxis:
    labels:
      datetimeFormatter:
        hour: HH
  plotOptions:
    bar:
      columnWidth: 100%
      barGap: 0
graph_span: 2d
show:
  last_updated: true
header:
  title: Energy Price
  show: true
  show_states: true
  colorize_states: true
span:
  start: day
now:
  show: true
  label: Now
series:
  - entity: sensor.electricity_prices_today
    yaxis_id: SEK
    type: column
    show:
      extremas: true
      in_header: false
    float_precision: 3
    color: green
    color_threshold:
      - value: 0
        color: "#00ffaa"
      - value: 0.25
        color: "#00ff55"
      - value: 0.5
        color: "#00ff00"
      - value: 0.75
        color: "#55ff00"
      - value: 1
        color: "#aaff00"
      - value: 1.5
        color: "#ffff00"
      - value: 2
        color: "#ffaa00"
      - value: 2.5
        color: "#ff5500"
      - value: 3
        color: "#ff0000"
      - value: 4
        color: "#ff0055"
      - value: 5
        color: "#ff00aa"
      - value: 6
        color: "#ff00ff"
      - value: 7
        color: "#ff34ff"
      - value: 9
        color: "#ff65ff"
      - value: 11
        color: "#ff98ff"
      - value: 13
        color: "#ffccff"
      - value: 15
        color: "#ffffff"
    data_generator: |
      const data = entity.attributes.data.map((entry) => {
        // Center each 15-minute period correctly
        const start = new Date(entry.start).getTime();
        const end = new Date(entry.end).getTime();
        const midpoint = start + (end - start) / 2;
        return [midpoint, entry.value];
      });
      return data;
  - entity: sensor.electricity_prices_tomorrow
    yaxis_id: SEK
    type: column
    show:
      extremas: true
      in_header: false
    float_precision: 3
    color: green
    color_threshold:
      - value: 0
        color: "#00ffaa"
      - value: 0.25
        color: "#00ff55"
      - value: 0.5
        color: "#00ff00"
      - value: 0.75
        color: "#55ff00"
      - value: 1
        color: "#aaff00"
      - value: 1.5
        color: "#ffff00"
      - value: 2
        color: "#ffaa00"
      - value: 2.5
        color: "#ff5500"
      - value: 3
        color: "#ff0000"
      - value: 4
        color: "#ff0055"
      - value: 5
        color: "#ff00aa"
      - value: 6
        color: "#ff00ff"
      - value: 7
        color: "#ff34ff"
      - value: 9
        color: "#ff65ff"
      - value: 11
        color: "#ff98ff"
      - value: 13
        color: "#ffccff"
      - value: 15
        color: "#ffffff"
    data_generator: |
      const data = entity.attributes.data.map((entry) => {
        // Center each 15-minute period correctly
        const start = new Date(entry.start).getTime();
        const end = new Date(entry.end).getTime();
        const midpoint = start + (end - start) / 2;
        return [midpoint, entry.value];
      });
      return data;    
  - entity: sensor.electricity_prices_today
    name: Current Price
    yaxis_id: SEK
    float_precision: 3
    show:
      in_header: true
      in_chart: false
    unit: Kr/kWh
    color: white
  - entity: sensor.electricity_prices_today
    attribute: min
    name: Min Today
    yaxis_id: SEK
    float_precision: 2
    show:
      in_header: true
      in_chart: false
    unit: Kr/kWh
    color: green
  - entity: sensor.electricity_prices_today
    attribute: mean
    name: Average Today
    yaxis_id: SEK
    float_precision: 2
    show:
      in_header: true
      in_chart: false
    unit: Kr/kWh
    color: yellow
  - entity: sensor.electricity_prices_today
    attribute: max
    name: Max Today
    yaxis_id: SEK
    float_precision: 2
    show:
      in_header: true
      in_chart: false
    unit: Kr/kWh
    color: red
yaxis:
  - id: SEK
    min: 0
    max: ~12
    apex_config:
      tickAmount: 6
      forceNiceScale: true
      title:
        text: SEK

A more advanced graph can be found below

2 Likes

Sorry for stealing you post, but do someone know what broke my min/max in the Apex chart? Cant get this to show :frowning:

type: custom:apexcharts-card
now:
  show: true
  label: Aktuellt pris
  color: var(--primary-color)
graph_span: 24h
show:
  last_updated: true
span:
  start: day
  offset: +0H
header:
  title: Elpriset idag/imorgon
  show: true
  show_states: true
  colorize_states: true
  floating: false
apex_config:
  show:
    offset_in_name: false
  chart:
    height: 250px
  legend:
    showForSingleSeries: true
  plotOptions:
    bar:
      borderRadius: 0
  yaxis:
    min: 0
    decimalsInFloat: 2
    tickAmount: 10
    forceNiceScale: true
hours_12: false
stacked: false
experimental:
  color_threshold: true
all_series_config:
  show:
    legend_value: false
    datalabels: false
    extremas: true
    in_brush: false
  float_precision: 1
  type: area
  invert: false
  fill_raw: zero
series:
  - entity: sensor.nordpool_kwh_se3_sek_3_10_0
    type: column
    color: lightblue
    color_threshold:
      - value: 0
        color: "#8be9fd"
      - value: 1
        color: "#50fa7b"
      - value: 1.5
        color: "#8be978"
      - value: 2
        color: "#f8f872"
      - value: 2.5
        color: "#ffb86c"
      - value: 3
        color: "#ff9859"
      - value: 3.5
        color: "#ff7846"
      - value: 4
        color: "#ff5555"
    float_precision: 2
    stroke_width: 0
    name: Idag
    unit: SEK/kWh
    show:
      legend_value: false
      extremas: true
      in_header: false
      header_color_threshold: true
    data_generator: |
      return entity.attributes.today.map((price, index) => {
        return [new Date().setHours(index/4,index%4*15,0), price];
      });
  - entity: sensor.nordpool_kwh_se3_sek_3_10_0
    attribute: min
    type: column
    float_precision: 2
    stroke_width: 2
    name: Lägsta
    show:
      in_chart: false
      in_header: true
      legend_value: false
      header_color_threshold: true
    color_threshold:
      - value: 0
        color: "#8be9fd"
      - value: 1
        color: "#50fa7b"
      - value: 1.5
        color: "#8be978"
      - value: 2
        color: "#f8f872"
      - value: 2.5
        color: "#ffb86c"
      - value: 3
        color: "#ff9859"
      - value: 3.5
        color: "#ff7846"
      - value: 4
        color: "#ff5555"
  - entity: sensor.nordpool_kwh_se3_sek_3_10_0
    name: Nuvarande
    type: column
    show:
      legend_value: false
      extremas: true
      in_header: true
      header_color_threshold: true
      in_chart: false
    float_precision: 2
    color_threshold:
      - value: 0
        color: "#8be9fd"
      - value: 1
        color: "#50fa7b"
      - value: 1.5
        color: "#8be978"
      - value: 2
        color: "#f8f872"
      - value: 2.5
        color: "#ffb86c"
      - value: 3
        color: "#ff9859"
      - value: 3.5
        color: "#ff7846"
      - value: 4
        color: "#ff5555"
  - entity: sensor.nordpool_kwh_se3_sek_3_10_0
    attribute: average
    type: column
    color: "#ffb86c"
    float_precision: 2
    stroke_width: 2
    name: Idag Medel
    show:
      in_chart: false
      in_header: false
      legend_value: false
  - entity: sensor.nordpool_kwh_se3_sek_3_10_0
    attribute: max
    type: column
    float_precision: 2
    stroke_width: 2
    name: Högsta
    show:
      in_chart: false
      in_header: true
      legend_value: false
      header_color_threshold: true
    color_threshold:
      - value: 0
        color: "#8be9fd"
      - value: 1
        color: "#50fa7b"
      - value: 1.5
        color: "#8be978"
      - value: 2
        color: "#f8f872"
      - value: 2.5
        color: "#ffb86c"
      - value: 3
        color: "#ff9859"
      - value: 3.5
        color: "#ff7846"
      - value: 4
        color: "#ff5555"
  - entity: sensor.nordpool_kwh_se3_sek_3_10_0
    color: "#6272a4"
    extend_to: false
    name: Imorgon
    unit: SEK/kWh
    float_precision: 2
    opacity: 0.5
    stroke_width: 2
    show:
      extremas: true
      in_header: false
      legend_value: false
    data_generator: |
      return entity.attributes.tomorrow.map((price, index) => {
        return [new Date().setHours(index/4,index%4*15,0), price];
      });

1 Like

Hi all,

Thanks about adwising ohters. I have also question… first, I’m not a newbie, but I’ve made programs only true interface automation, not yaml-code directly. So, is there any way to get that 15min period nordpool energy price from that Nordpool Hacs, or do I have to make a code to the yaml side? And do You guys some ready simble code for yaml about that?

Kindly,
Aleksi S

It is in the first set of posts…and it is not (!) using hacs version, follow the link

Hi,

ok. I have tried, but not successed:(
But, am I understand correctly… if I just wait, the the Nordpool integration starts to spit 15 prices automatically in the future? (see picture).

If you use the non-hacs nordpool there is an action (fka service-call) that extracts the prices per 15min. , posts above show how to do this

Thanks for sharing this solution. I am using the core nordpool integration and have copied your second template version in my configuration.yaml but the sensor shows up as ‘Unknown’ and without attributes.

Does anyone have an idea what may be going wrong?

Thanks

PS Not sure if this is relevant, but I have separately setup a hub directly in the integration that fetches some data for the Netherlands, while your template of course is setup to fetch data for Sweden. I assumed that these are independent but am not sure. I have also setup the two input_numbers for tax and purchase costs per your specification.

EDIT: I failed to realise that I needed to enter my own config_entry code. Using the correct one fixed the issue.

try copying the code again. i’ve changed some stuff. (remember to change the config entry and such)

the hacs version gave min and max and such too but i have not made that in this template yet.

also the data gets quite big when today and tomorrow prices are present so ha logs says 16k limit bladiblah… one solution is to derive the price sensor to a separate entity… i might have a look at that later. but try the code above until then.

also. the nordpool integration seem to have a bug where it only gives 1h price for current price. tested with derived current price sensor but still 1h prices. so waiting for a bug fix for that. graphs and price automation works well thou.

updated and added some scripts and graph options.

1 Like

Thanks a lot for sharing these scripts. I am so glad I do not have to figure this out myself!
I just wanted to note that for me (on core version 2025.10.1), the nordpool current price sensor updates every 15 minutes

Great stuff!
I have a question though. Somewhere along the line, you introduced the attribute tomorrow_valid, presumably to format the graph in case of absence of values for the next day. The check for this is if tomorrow_price is mapping. Currently, the tomorrow_price is an empty list and it still returns TRUE.

Could it be that the response of the action to get prices from Nord pool is an object like: { "NL": [] }? This is then a mapping in Jinja, so it returns TRUE.

I changed the code a bit:

{{ tomorrow_price is mapping }}

allright. i have not upgradet yet. the min max and avg calculations might come in handy thou.

edit: upgraded and verified. yes the nordpol integration now gives 15 min data for state too. great! i’ll see if i can bake the min max and avg calc into the table template.

great! i’ve taken help of ai to write/modify the code and then testing it a lot myself. this might have slipped thru my tests. thanks for checking the code. the attribute is just to mimic the original attributes from the hacs version. it might be unnecessary

remade the checks in both the tomorrow valid part and table conversion to check for real data. seem to work. scripts and graph updated.

this thread is for the built in nordpool integration. not hacs that you seem to use.

1 Like

Oh i see, thanks, is there any big differences for them both? I tried built in before but it didnt work as good as the HACS version… :confused: