Bringing electricity information from eloverblik.dk and energidataservice.dk into Openhab

Hello,

I have been eager to get real time information on Danish electricity prices into Openhab for potential automation - inspired by this Rest-API setup in openhab. In Denmark it is possible to create an account into https://watts.dk/ for example and use their app to see both real time electricity prices (for your concrete electricity meter) as well as 3 days delayed usage statistics. This is nice if you can remember to check if you should start the washing machine now or set it for 3 hours later but let’s face it… who can remember such simple things. Wouldn’t it be better to sink some hours into automation and never check again? :slight_smile:

Hang in there - this will be long.

What WILL we do?

  1. Get real time data for the electricity prices - valid for the Flex plans → this will come from energidataservice.dk
  2. fetch all applicable charges and fees (distribution and state) for YOUR specific meter - this will come from eloverblik.dk
  3. add VAT to all values (25% in Denmark)
  4. compute total current running cost of electricity in Danish kroner (DKK) per kWh
  5. show all of the above nicely in an Openhab sitemap/android app

What we WILL NOT do (but i will try to create later on and would appreciate co-conspirators!)

As i started without knowing much about the electricity market in Denmark i figure a recap of my learning will give a good context of why things are structured the way they are:

  • in Denmark (and many other EU countries) there is a law that all electricity, gas and other data is public (as annonimized averages)
  • monster databases exist showing real time and historical data of electricity generation/consumption/prices, gas storage/consumption etc. - this is at https://www.energidataservice.dk/
  • all information shown here is available freely at https://www.energidataservice.dk/ yet some is pulled from https://www.eloverblik.dk where a log in is required - why? because https://www.eloverblik.dk has all charges filtered by validity and because my plan was to pull usage data as well
  • https://www.eloverblik.dk itself is an electricity self-service portal where your own electricity meter is accessible behind a log in page - that is not public obviously.
  • in Denmark there are three main things that contribute to the total cost of electricity - state transportation taxes (country level - you pay to the government), local transportation taxes (municipality - you pay to the local electricity distribution company - N1 in my case) and of course actual electricity cost (you pay to the people who buy the electricity from the common exchange and deliver to you and make up the final bill for all parties - Norlys in my case). All these can be fetched from https://www.energidataservice.dk/ if you know exactly what to look for. See this video (in Danish) for explanation - Hvordan er din elregning skruet sammen? - ForstĂĽ din elregning her
  • you need to log in to https://www.eloverblik.dk and register your electricity meter and register an API refresh token - see this guide: https://energinet.dk/media/gmgpcgfm/eloverblik-tredjeparters-adgang-til-kunders-data-via-api-kald.pdf
  • at the end of this preparation step you should have: refresh token (a very long string), the metering point of your electricity meter (18 digits long number, which we will store as string because that’s how we will use it in the REST API) - both from https://www.eloverblik.dk

Ok let’s finally begin → Items. Everything is under single equipment group. The first two items hold the two fixed values - refresh token and metering point - everything else i dynamically pulled. GLN numbers are the IDs of who gets your money. They can be used to fetch the same data from https://www.energidataservice.dk/

eloverblik.items

Group geEloverblikAPI "Eloverblik API" <energy> (Apt_Facility) ["Equipment"]

// Basic inputs - states updated on Openhab start and whenever NULL for whatever reason
String Eloverblik_metering_point "Metering Point [%s]" <settings> (geEloverblikAPI) ["Status"]
String Eloverblik_refresh_token "Refresh token [%s]" <settings> (geEloverblikAPI) ["Status"]

// Populated by get_eloverblik_access_token.script on openhab start up and every 12 hours after that
String Eloverblik_access_token "Access Token [%s]" <settings> (geEloverblikAPI) ["Status"]

// Updated by get_eloverblik_charges.script on openhab start up and every day at midnight after that
// Fees
// this section is empty on purpose.

// Subscribtions
Number Eloverblik_subscription_price "Subscription price [%.3 DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_subscription_price_discount "Subscription price discount [%.3f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
String Eloverblik_subscription_gln_number "Subscription GLN Number [%s]" <suitcase> (geEloverblikAPI) ["Status"]
String Eloverblik_subscription_name "Subscription name [%s]" <suitcase> (geEloverblikAPI) ["Status"]
String Eloverblik_subscription_description "Subscription description [%s]" <suitcase> (geEloverblikAPI) ["Status"]

// Tariffs

// Nettarif C
Number Eloverblik_nettarif_c_price_0_1 "Nettarif C price 0_1 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_1_2 "Nettarif C price 1_2 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_2_3 "Nettarif C price 2_3 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_3_4 "Nettarif C price 3_4 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_4_5 "Nettarif C price 4_5 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_5_6 "Nettarif C price 5_6 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_6_7 "Nettarif C price 6_7 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_7_8 "Nettarif C price 7_8 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_8_9 "Nettarif C price 8_9 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_9_10 "Nettarif C price 9_10 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_10_11 "Nettarif C price 10_11 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_11_12 "Nettarif C price 11_12 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_12_13 "Nettarif C price 12_13 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_13_14 "Nettarif C price 13_14 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_14_15 "Nettarif C price 14_15 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_15_16 "Nettarif C price 15_16 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_16_17 "Nettarif C price 16_17 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_17_18 "Nettarif C price 17_18 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_18_19 "Nettarif C price 18_19 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_19_20 "Nettarif C price 19_20 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_20_21 "Nettarif C price 20_21 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_21_22 "Nettarif C price 21_22 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_22_23 "Nettarif C price 22_23 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_23_24 "Nettarif C price 23_24 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
String Eloverblik_nettarif_c_gln_number "Nettarif C GLN Number [%s]" <suitcase> (geEloverblikAPI) ["Status"]

// Nettarif C discount
Number Eloverblik_nettarif_c_price_0_1_disc "Nettarif C discount price 0_1 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_1_2_disc "Nettarif C discount price 1_2 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_2_3_disc "Nettarif C discount price 2_3 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_3_4_disc "Nettarif C discount price 3_4 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_4_5_disc "Nettarif C discount price 4_5 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_5_6_disc "Nettarif C discount price 5_6 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_6_7_disc "Nettarif C discount price 6_7 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_7_8_disc "Nettarif C discount price 7_8 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_8_9_disc "Nettarif C discount price 8_9 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_9_10_disc "Nettarif C discount price 9_10 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_10_11_disc "Nettarif C discount price 10_11 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_11_12_disc "Nettarif C discount price 11_12 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_12_13_disc "Nettarif C discount price 12_13 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_13_14_disc "Nettarif C discount price 13_14 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_14_15_disc "Nettarif C discount price 14_15 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_15_16_disc "Nettarif C discount price 15_16 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_16_17_disc "Nettarif C discount price 16_17 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_17_18_disc "Nettarif C discount price 17_18 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_18_19_disc "Nettarif C discount price 18_19 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_19_20_disc "Nettarif C discount price 19_20 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_20_21_disc "Nettarif C discount price 20_21 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_21_22_disc "Nettarif C discount price 21_22 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_22_23_disc "Nettarif C discount price 22_23 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_nettarif_c_price_23_24_disc "Nettarif C discount price 23_24 [%.6f DKK]" <piggybank> (geEloverblikAPI) ["Status"]

// Transmissions nettarif
Number Eloverblik_nettarif_price "Nettarif price [%.3f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
String Eloverblik_nettarif_gln_number "Nettarif GLN Number [%s]" <suitcase> (geEloverblikAPI) ["Status"]

// Systemtarif
Number Eloverblik_systemtarif_price "Systemtarif price [%.3f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
String Eloverblik_systemtarif_gln_number "Systemtarif GLN Number [%s]" <suitcase> (geEloverblikAPI) ["Status"]

//Elafgift
Number Eloverblik_elafgift_price "Elafgift price [%.3f DKK]" <piggybank> (geEloverblikAPI) ["Status"]
String Eloverblik_elafgift_gln_number "Elafgift GLN Number [%s]" <suitcase> (geEloverblikAPI) ["Status"]

// Updated by update_electricity_items.script on openhab start up and every hour after that
// Totals
DateTime  Eloverblik_last_check "Last Check [%1$tR on %1$ta, %1$tb %1$td %1$tY]" <time> (geEloverblikAPI) ["Timestamp"]
Number Eloverblik_total_transport "Transport [%.2f DKK/kWh]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_total_taxes "Taxes [%.2f DKK/kWh]" <piggybank> (geEloverblikAPI) ["Status"]
Number Eloverblik_total_electricity "Electricity [%.2f DKK/kWh]" <piggybank> (geEloverblikAPI) ["Status"]

Number Eloverblik_total_cost "Total Cost [%.2f DKK/kWh]" <piggybank> (geEloverblikAPI) ["Status"]

With the items such defined we can proceed to the base rules using them.

eloverblik.rules

val refresh_token = "a stupidly long refresh token string goes here - you got that by making it for a third party API app in https://www.eloverblik.dk/"
val metering_point = "your 18 digit long metering point from https://www.eloverblik.dk/ goes here "

rule "Update Electricity Items"
when
    Time cron "0 0 * * * ? *" // once an hour
then
    if (Eloverblik_refresh_token.state==NULL) {
        postUpdate(Eloverblik_refresh_token, refresh_token)
        callScript("get_eloverblik_access_token")
    }
    if (Eloverblik_metering_point.state==NULL) {
        postUpdate(Eloverblik_metering_point, metering_point)
        callScript("get_eloverblik_charges")
    }

    callScript("update_electricity_items")
end

rule "Eloverblik initialization"
when
    System started
then
    // First update the items with proper state values
    postUpdate(Eloverblik_refresh_token, refresh_token)
    postUpdate(Eloverblik_metering_point, metering_point)

    // Then update the access token
    callScript("get_eloverblik_access_token")

    // Then update the charges
    callScript("get_eloverblik_charges")

    // Then pull the current spot electricity prices
    callScript("update_electricity_items")

end

rule "Update Eloverblik access token"
when
    Time cron "0 0 0/12 * * ? *" // every 12 hours at 0 and 12 oclock
then
    if (Eloverblik_refresh_token.state==NULL) postUpdate(Eloverblik_refresh_token, refresh_token)
    callScript("get_eloverblik_access_token")
end

rule "Update Eloverblik charges"
when
    Time cron "0 0 0 * * ? *" // once a day at midnight
then
    if (Eloverblik_metering_point.state==NULL) postUpdate(Eloverblik_metering_point, metering_point)
    callScript("get_eloverblik_charges")
end

Then come the scripts:
get_eloverblik_access_token.script:

val headers = newHashMap("Authorization" -> "Bearer " + Eloverblik_refresh_token.state.toString,
    "Accept" -> "application/json")
var result = sendHttpGetRequest("https://api.eloverblik.dk/CustomerApi/api/Token", headers, 3000)
postUpdate(Eloverblik_access_token, transform("JSONPATH", "$.result", result))

get_eloverblik_charges.script

val headers = newHashMap("Authorization" -> "Bearer " + Eloverblik_access_token.state,
                            "Accept" -> "application/json",
                            "WWW-Authenticate" -> "Basic")
val body = '{"meteringPoints": {"meteringPoint": ["' + Eloverblik_metering_point.state.toString + '"]}}'
val contentType = "application/json"

// sendHttpPostRequest(String url, String contentType, String content, Map<String, String> headers, int timeout)
var result = sendHttpPostRequest("https://api.eloverblik.dk/CustomerApi/api/meteringpoints/meteringpoint/getcharges",
                                    contentType, body, headers, 3000)

// Subscriptions updates
postUpdate(Eloverblik_subscription_price, transform("JSONPATH", ".result[0].result.subscriptions[0].price", result))
postUpdate(Eloverblik_subscription_price_discount, transform("JSONPATH", ".result[0].result.subscriptions[1].price", result))
postUpdate(Eloverblik_subscription_gln_number, transform("JSONPATH", ".result[0].result.subscriptions[0].owner", result))
postUpdate(Eloverblik_subscription_name, transform("JSONPATH", ".result[0].result.subscriptions[0].name", result))
postUpdate(Eloverblik_subscription_description, transform("JSONPATH", ".result[0].result.subscriptions[0].description", result))

// Tariffs updates

// Nettarif C
postUpdate(Eloverblik_nettarif_c_price_0_1, transform("JSONPATH", ".result[0].result.tariffs[0].prices[0].price", result))
postUpdate(Eloverblik_nettarif_c_price_1_2, transform("JSONPATH", ".result[0].result.tariffs[0].prices[1].price", result))
postUpdate(Eloverblik_nettarif_c_price_2_3, transform("JSONPATH", ".result[0].result.tariffs[0].prices[2].price", result))
postUpdate(Eloverblik_nettarif_c_price_3_4, transform("JSONPATH", ".result[0].result.tariffs[0].prices[3].price", result))
postUpdate(Eloverblik_nettarif_c_price_4_5, transform("JSONPATH", ".result[0].result.tariffs[0].prices[4].price", result))
postUpdate(Eloverblik_nettarif_c_price_5_6, transform("JSONPATH", ".result[0].result.tariffs[0].prices[5].price", result))
postUpdate(Eloverblik_nettarif_c_price_6_7, transform("JSONPATH", ".result[0].result.tariffs[0].prices[6].price", result))
postUpdate(Eloverblik_nettarif_c_price_7_8, transform("JSONPATH", ".result[0].result.tariffs[0].prices[7].price", result))
postUpdate(Eloverblik_nettarif_c_price_8_9, transform("JSONPATH", ".result[0].result.tariffs[0].prices[8].price", result))
postUpdate(Eloverblik_nettarif_c_price_9_10, transform("JSONPATH", ".result[0].result.tariffs[0].prices[9].price", result))
postUpdate(Eloverblik_nettarif_c_price_10_11, transform("JSONPATH", ".result[0].result.tariffs[0].prices[10].price", result))
postUpdate(Eloverblik_nettarif_c_price_11_12, transform("JSONPATH", ".result[0].result.tariffs[0].prices[11].price", result))
postUpdate(Eloverblik_nettarif_c_price_12_13, transform("JSONPATH", ".result[0].result.tariffs[0].prices[12].price", result))
postUpdate(Eloverblik_nettarif_c_price_13_14, transform("JSONPATH", ".result[0].result.tariffs[0].prices[13].price", result))
postUpdate(Eloverblik_nettarif_c_price_14_15, transform("JSONPATH", ".result[0].result.tariffs[0].prices[14].price", result))
postUpdate(Eloverblik_nettarif_c_price_15_16, transform("JSONPATH", ".result[0].result.tariffs[0].prices[15].price", result))
postUpdate(Eloverblik_nettarif_c_price_16_17, transform("JSONPATH", ".result[0].result.tariffs[0].prices[16].price", result))
postUpdate(Eloverblik_nettarif_c_price_17_18, transform("JSONPATH", ".result[0].result.tariffs[0].prices[17].price", result))
postUpdate(Eloverblik_nettarif_c_price_18_19, transform("JSONPATH", ".result[0].result.tariffs[0].prices[18].price", result))
postUpdate(Eloverblik_nettarif_c_price_19_20, transform("JSONPATH", ".result[0].result.tariffs[0].prices[19].price", result))
postUpdate(Eloverblik_nettarif_c_price_20_21, transform("JSONPATH", ".result[0].result.tariffs[0].prices[20].price", result))
postUpdate(Eloverblik_nettarif_c_price_21_22, transform("JSONPATH", ".result[0].result.tariffs[0].prices[21].price", result))
postUpdate(Eloverblik_nettarif_c_price_22_23, transform("JSONPATH", ".result[0].result.tariffs[0].prices[22].price", result))
postUpdate(Eloverblik_nettarif_c_price_23_24, transform("JSONPATH", ".result[0].result.tariffs[0].prices[23].price", result))
postUpdate(Eloverblik_nettarif_c_gln_number, transform("JSONPATH", ".result[0].result.tariffs[0].owner", result))

// Nettarif C discount
postUpdate(Eloverblik_nettarif_c_price_0_1_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[0].price", result))
postUpdate(Eloverblik_nettarif_c_price_1_2_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[1].price", result))
postUpdate(Eloverblik_nettarif_c_price_2_3_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[2].price", result))
postUpdate(Eloverblik_nettarif_c_price_3_4_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[3].price", result))
postUpdate(Eloverblik_nettarif_c_price_4_5_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[4].price", result))
postUpdate(Eloverblik_nettarif_c_price_5_6_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[5].price", result))
postUpdate(Eloverblik_nettarif_c_price_6_7_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[6].price", result))
postUpdate(Eloverblik_nettarif_c_price_7_8_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[7].price", result))
postUpdate(Eloverblik_nettarif_c_price_8_9_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[8].price", result))
postUpdate(Eloverblik_nettarif_c_price_9_10_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[9].price", result))
postUpdate(Eloverblik_nettarif_c_price_10_11_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[10].price", result))
postUpdate(Eloverblik_nettarif_c_price_11_12_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[11].price", result))
postUpdate(Eloverblik_nettarif_c_price_12_13_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[12].price", result))
postUpdate(Eloverblik_nettarif_c_price_13_14_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[13].price", result))
postUpdate(Eloverblik_nettarif_c_price_14_15_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[14].price", result))
postUpdate(Eloverblik_nettarif_c_price_15_16_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[15].price", result))
postUpdate(Eloverblik_nettarif_c_price_16_17_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[16].price", result))
postUpdate(Eloverblik_nettarif_c_price_17_18_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[17].price", result))
postUpdate(Eloverblik_nettarif_c_price_18_19_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[18].price", result))
postUpdate(Eloverblik_nettarif_c_price_19_20_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[19].price", result))
postUpdate(Eloverblik_nettarif_c_price_20_21_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[20].price", result))
postUpdate(Eloverblik_nettarif_c_price_21_22_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[21].price", result))
postUpdate(Eloverblik_nettarif_c_price_22_23_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[22].price", result))
postUpdate(Eloverblik_nettarif_c_price_23_24_disc, transform("JSONPATH", ".result[0].result.tariffs[1].prices[23].price", result))

// Transmissions nettarif
postUpdate(Eloverblik_nettarif_price, transform("JSONPATH", ".result[0].result.tariffs[2].prices[0].price", result))
postUpdate(Eloverblik_nettarif_gln_number, transform("JSONPATH", ".result[0].result.tariffs[2].owner", result))

// Systemtarif
postUpdate(Eloverblik_systemtarif_price, transform("JSONPATH", ".result[0].result.tariffs[3].prices[0].price", result))
postUpdate(Eloverblik_systemtarif_gln_number, transform("JSONPATH", ".result[0].result.tariffs[3].owner", result))

// Elafgift
postUpdate(Eloverblik_elafgift_price, transform("JSONPATH", ".result[0].result.tariffs[4].prices[0].price", result))
postUpdate(Eloverblik_elafgift_gln_number, transform("JSONPATH", ".result[0].result.tariffs[4].owner", result))

update_electricity_items.script → in the url for the API call below I use DK1 for west of Denmark - change to DK2 for CPH and east of Denmark.

// import org.openhab.core.model.script.ScriptServiceUtil
// Updating meta Items
val vat = 0.25
val ZonedDateTime currentJavaLocalDateTime = now

val current_time_transport = org.openhab.core.model.script.ScriptServiceUtil.getItemRegistry.getItem("Eloverblik_nettarif_c_price_" + currentJavaLocalDateTime.getHour().toString + "_" + (currentJavaLocalDateTime.getHour() + 1).toString)
val current_time_transport_disc = org.openhab.core.model.script.ScriptServiceUtil.getItemRegistry.getItem("Eloverblik_nettarif_c_price_" + currentJavaLocalDateTime.getHour().toString + "_" + (currentJavaLocalDateTime.getHour() + 1).toString + "_disc")

val transport = current_time_transport.state as Number
val transport_disc = current_time_transport_disc.state as Number

val nettarif = Eloverblik_nettarif_price.state as Number
val systemtarif = Eloverblik_systemtarif_price.state as Number
val elafgift = Eloverblik_elafgift_price.state as Number

val headers = newHashMap("accept" -> "*/*")
var result = sendHttpGetRequest("https://api.energidataservice.dk/dataset/Elspotprices?start=now-PT1h&filter=%7B%22PriceArea%22%3A%20%22DK1%22%7D&sort=HourDK", headers, 3000)
var electricity_str = transform("JSONPATH", ".records[0].SpotPriceDKK", result)
var float electricity = Float::parseFloat(String::format("%s",electricity_str).replace(' ',''))

var total_transport = (transport + transport_disc) + (transport + transport_disc) * vat
var total_taxes = (nettarif + systemtarif + elafgift) + (nettarif + systemtarif + elafgift) * vat
var electricity_per_kWh = (electricity / 1000) + (electricity / 1000) * vat

postUpdate(Eloverblik_total_transport, total_transport)
postUpdate(Eloverblik_total_taxes, total_taxes)
postUpdate(Eloverblik_total_electricity, electricity_per_kWh)
postUpdate(Eloverblik_total_cost, (total_transport + total_taxes + electricity_per_kWh))
postUpdate(Eloverblik_last_check, DateTimeType.valueOf(currentJavaLocalDateTime.toLocalDateTime().toString()))

And finally the sitemap:

Group item=Eloverblik_total_cost icon="energy" {
			Frame label="Electricity Costs" icon="energy" {
				Text item=Eloverblik_total_cost
				Text item=Eloverblik_total_electricity
				Text item=Eloverblik_total_transport
				Text item=Eloverblik_total_taxes
				Text item=Eloverblik_last_check  label="Last Update [%1$tR on %1$ta, %1$tb %1$td %1$tY]"
			}

With an end result:
image

Random thoughts and questions to the experts in case anyone makes this far down…:

  • when working with offline Openhab instance you need the following IPs to be allowed through the firewall: 40.118.61.157 for energidataservice.dk and 13.107.238.44 for eloverblik.dk
  • i struggled with the thought of dumping stuff into persistnace but it felt wastefull to pull form the internet only to store locally… But how to plot if not from store? Perhaps Grafana i best with JSON data source? Ideas?
  • why does import org.openhab.core.model.script.ScriptServiceUtil work in rules but not in scripts?
  • why does running the same val ZonedDateTime currentJavaLocalDateTime = now seems to be 1 hours offset between being run in rules vs script? It seems the script does not get the timezone +1 for DK and uses UTC…
3 Likes

Nice work, how do you handle the different tarif’s doing 24hours, is it done en eloverblik.dk ?

Mvh Mads

Hi Mads,

There are two time dependent values:
1 the transport price by the local network operator - N1 for example
2 the actual time varying electricity price from the open exchange

1: I load all values from eloverblik.dk with the get_eloverblik_charges.script. There are 48 values - 24 tariffs for each of the 24 hours of the day and 24 discounts (if any). These are semi-static with prices changing once every 2-3 months, therefore i only check those once a day. They also don’t depend day to day. I pick the right one by using the Openhab time of the query, getting the current hour and selecting from the set of values for each hour.
this bit:

val ZonedDateTime currentJavaLocalDateTime = now

val current_time_transport = org.openhab.core.model.script.ScriptServiceUtil.getItemRegistry.getItem("Eloverblik_nettarif_c_price_" + currentJavaLocalDateTime.getHour().toString + "_" + (currentJavaLocalDateTime.getHour() + 1).toString)
val current_time_transport_disc = org.openhab.core.model.script.ScriptServiceUtil.getItemRegistry.getItem("Eloverblik_nettarif_c_price_" + currentJavaLocalDateTime.getHour().toString + "_" + (currentJavaLocalDateTime.getHour() + 1).toString + "_disc")

val transport = current_time_transport.state as Number
val transport_disc = current_time_transport_disc.state as Number

2: the current price of electricity depends on many factors (demand, supply, wind blowing or not) but the smallest cadence of update is one hour so i read these once an hour from energidataservice.dk. This is the exact dataset - ENERGI DATA SERVICE . When read with start=now option it fetches all values from now until available and the &sort=HourDK option gives them in ascending order so i only pick the first one for the current time.

Does this answer your question?

cheers

Great work, and thanks for sharing.

I can add that I’m working on something similar - complimentary, not conflicting. :slight_smile: My first step was creating the dishwasher rule you mentioned - using the HTTP binding for fetching the data.

In last weekend I decided to take this a step further, so I started working on next step which is a binding for fetching the data. This is intended to replace the HTTP binding usage in my rule as well as the direct HTTP calls in your rules for Energi Data Service. This will have the following benefits (when completed):

  • Unified and reusable interface to the data to be consumed by rules.
  • Logic for making as few calls as possible and get data as fast as possible.
  • Error handling (retry policies).
  • Actions for common calculations to be used by rules.

The work can be tracked here:

I have also been playing with the eloverblik.dk API in Postman, and was able to pull interesting data like you have also shown here. I switched focus for a moment because my initial interest was getting electricity meter data, which I in the meantime found a better solution for:
https://techblog.vindvejr.dk/?p=523

However, with the new tariff models introduced this month, eloverblik.dk is again interesting and needed for calculating the gross cost taking everything into consideration. So when my current Energi Data Service is a little bit more evolved, I plan to proceed and create a similar binding for eloverblik.dk.

The next steps that could be interesting would be to have a look at openHAB itself and come up with a proposal for an energy API in core, so that bindings could implement external services and expose the data to core in a unified way, i.e. through some common interfaces.

This way rules could potentially interact with different underlying services in a common way. Similarly, common calculations could be implemented just once in core.

Obviously this require a good amount of work in order to discuss, design and implement this. Some initial thoughts can be found here:

2 Likes

Hi Jacob,

Excellent initiative and I’d gladly join you and attempt to help. I of course agree a binding is best. I actually struggled with the HTTP binding and some jython scripting before i realized their limitations and going for actions - i just could not figure out the handover of the access token for eloverblik without a variable to store into. My biggest issue is that i know very little java - python is much easier for me. I almost switched to HA because of that… I don’t know how useful i can be on the writing of the binding but i can test :).

I also saw your Omnipower solution - that is my priority No1 when i move to my new house on Feb 1st :slight_smile: . I think the only thing i don’t like about that solution is the ESP based bridge but i can see how that is just easy to implement so I’ll probably do the same as you. Any hints as to what to tell N1 would be welcome - last time i talked to them they did not know what i was talking about.

I will read the threads you posted and try the binding you have already to give you feedback. As initial thought i can say that I think you can stay with energidataservice.dk alone as i was able to map all data from eloverblik.dk to this dataset: ENERGI DATA SERVICE . The main issue is to get your filters right and eloverblik.dk has that for free. So from user point of view it will be either:

  • get 4-5 values for GLN/tarif type etc and use only energidataservice.dk
    OR
  • use eloverblik.dk with token and metering point for the state and local network company charges (as i did in my scripts) and then spot prices from energidataservice.dk as you have also done.

The main reason to connect to eloverblik.dk beyond the filtered charges is the metering data. I checked Grafana and there you can actually store a variable key and handover the access token. So for just pulling data and displaying (like in watts.dk) grafana would be fine i think - have not actually implemented it yet. The binding would be needed for all the calculations, which i cannot currently see an easy way to implement in grafana.

Finally on the topic of core/openhab feature - i know nothing about that - i trust you will come up with something meaningful. :slight_smile: Just keep in mind I might be putting solar panels next summer :slight_smile: .

Cheers

Curious, which problem do you see in the ESP-based solution?

You can use this form:

You need to provide your meter number and mention that you need the GPK 60 and GPK 61 keys, and probably also that you’ll be needing push, so this should be activated on your meter. They will usually get back to you within a few working days. I believe it was only in December that N1 became capable of providing these keys and activating push. I don’t know all the details, perhaps they needed to update the firmware, or maybe that were just not internally ready to handle this.

It’s wireless (in general i prefer cable for everything) and judging by the few other ESPs i have in the house not a very reliable one. I also run tasmota on few and i keep having issues. I know it is not a popular opinion but it is my experience. I am not a pro but i am also not a complete newby so if it does take an absolute pro to get them to be reliable then they are not for me. But i do recognize the enormous community around them and the many projects they enable so more power to them - if i can avoid them i will but if not then so be it - i will use them.

Thanks a lot! Did you investigate the now discontinued Kamstrup HAN module?

I also prefer cabling in general. In this case I can live with a Wi-Fi solution, since it’s very low bandwidth and it’s close to my access point and packet loss is not critical since data is sent every 10 seconds. It also saves some power by not using a port on my switch. :slight_smile:

So far the solution seems very stable and reliable, I haven’t experience any issues.

No.

A small update on the Energi Data Service binding: During the last week the work has progressed a lot. There are now channels for all prices, so I think it can now replace some of your rules. I also backported it to Java 11 and openHAB 3.4, so it can be tested in production.

Nice work!

You might be interested to check this solution for some inspiration:

I optimized our house heating and hot water heating based on the spot prices so that it’s done during the cheap hours of the day. I’ve managed to save 400€ between August and December with this solution so far.

Markus

2 Likes

I use HTTP binding to poll raw data from NordPool, and then in my rules I add a fees (currently hard-coded) and VAT on top. Currently I use this to schedule charging my car. However I’m interested to pull usage data from eloverblik, thanks a lot for sharing!

Hi @somy ,

This is now a deprecated solution as far as I can tell. You should check the evolution of Jacobs binding, which I believe has far surpassed the functionality here.

Cheers

Hi @katerica ,

Thank you for your reply! I’m a bit confused about what data is provided by which websites, and I’m not reading Danish which makes things a little harder for me. Initially I thought eloverblik.dk only provides usage data, but it sounds like it also can provide tax and fees for calculating final price - does it take into consideration of el provider (not grid provider) which also can add a margin on top of raw price?
I have never tried https://www.energidataservice.dk/, what data do they provide?
I ask because today I poll raw price from NordPool (simple HTTP binding works reliably), and I created some rules to calculate full price based on formula I find here: https://elspotpris.dk/
This solution seems to work reliably for me, and the good thing is I know el price tomorrow from 13:00 everyday. Does eloverblik and energidataservice also can tell future prices?

Many thanks all for the good discussion and suggestions!

No, that part is a jungle. They would then need to know your exact contract with your electricity supplier. As an example, I was recently in contact with my supplier Vindstød, asking if the 0,063 øre/kWh I’m paying for the “LokalVind” subscription is including or excluding VAT, because that wasn’t declared at their webpage. It turned out I had an older subscription to “LokalVind” which was more expensive. So they switched me to “LokalVind” (the new one) where I had to pay 0,05 øre/kWh excl. VAT, i.e. 0,0625 øre, which rounded up resembled my original assumption.

You would need to go there and look around. They provide tons of datasets! For electricity, the most interesting ones for you as a private consumer would probably be Elspotprices and DatahubPricelist. These are also the two datasets implemented in the Energi Data Service binding.

They would prefer that you didn’t do that. :slightly_smiling_face: At least I’ve heard that they would prefer end users querying other services rather than everyone going directly to the “root”.

Yes, both of them. I believe you can simplify your setup and have an even more robust querying by using the Energi Data Service binding, which should be available for you to install directly from Community Marketplace (the pull request is waiting for review). The documentation is here.

With this binding you will also get future prices around 13:00, and it’s easy to set up a group item with the total costs - in combination with the VAT transformation that you can also install from Community Marketplace.

1 Like

Hi @laursen
Thank you for your reply with lots of useful info, very much appreciated. Regarding the el provider I took the offer from elforbundet and the margin is 0 as I read.
I will definitely try the binding as I don’t particularly like to hard code all the fees and tax in rules - they’re not static and can change from month to month.
I learned a lot from this post, I will now also use the el price to turn on/off Nilan central heating and hot water production depends on el price (inspired by @masipila ) The only concern I have is whether frequent turn on/off heating and hot water has any impact to the heat pump. I’m aware that I shouldn’t turn off ventilation as condensation can build up in the pipes.

Hi @laursen

I have tried your binding and it’s smooth to set up and works great except for one little issue. I don’t seem to get value for the below item, it says “UNDEF”:

Number NetTariff "Current Net Tariff" <price> (TotalPrice) { channel="energidataservice:service:energidataservice:electricity#net-tariff" [profile="transform:VAT"] }

Same in HourlyPrices I don’t see Net Tariff:

{"hourStart":"2023-05-10T19:00:00Z","spotPrice":0.229600006,"spotPriceCurrency":"DKK","systemTariff":0.054000,"electricityTax":0.008000,"transmissionNetTariff":0.058000}

My things configuration below:

Thing energidataservice:service:energidataservice "Energi Data Service" [ priceArea="DK2", currencyCode="DKK", gridCompanyGLN="5790000705689" ] {
    Channels:
        Number : electricity#net-tariff [ chargeTypeCodes="DT_C_01", start="StartOfMonth" ]}

The Data feed looks like the following:

ChargeOwner GLN_Number ChargeType ChargeTypeCode Note Description ValidFrom ValidTo VATClass Price1 Price2 Price3 Price4 Price5 Price6 Price7 Price8 Price9 Price10 Price11 Price12 Price13 Price14 Price15 Price16 Price17 Price18 Price19 Price20 Price21 Price22 Price23 Price24 TransparentInvoicing TaxIndicator ResolutionDuration
Radius Elnet A/S 5790000705689 D03 DT_C_01 Nettarif C time Nettarif C time 01/10/2024 D02 0,1509 0,1509 0,1509 0,1509 0,1509 0,1509 0,4528 0,4528 0,4528 0,4528 0,4528 0,4528 0,4528 0,4528 0,4528 0,4528 0,4528 1,3584 1,3584 1,3584 1,3584 0,4528 0,4528 0,4528 0 0 PT1H
Radius Elnet A/S 5790000705689 D03 DT_C_01 Nettarif C time Nettarif C time 01/04/2024 01/10/2024 D02 0,1509 0,1509 0,1509 0,1509 0,1509 0,1509 0,2264 0,2264 0,2264 0,2264 0,2264 0,2264 0,2264 0,2264 0,2264 0,2264 0,2264 0,5887 0,5887 0,5887 0,5887 0,2264 0,2264 0,2264 0 0 PT1H

A potential feature request if you don’t mind: today I created 48 items grouped in Today and Tomorrow groups, and I update total prices when they become available. Then every night I copy Tomorrow prices to Today and set Tomorrow price to empty (please see the screenshot below). In the current binding looks like all hourly prices are stored in Json, I think it’ll be more convenient to provide channels for each hour (like TotalPrice but for 48 hours) instead so that the data is more structured and easy to validate.

Many thanks in advance!

Edit: I found out how to configure the filter however doesn’t seem to work, attached the raw data for Radius

You do not need to configure this filter manually, the binding comes with pre-configured filters, also for Radius. If you remove the channel configuration, it should work. The start parameter is wrong since it will exclude records still valid, but starting before the beginning of current month. The pre-configured filter has 2023-01-01 as cutoff date.

You can also see this in the records you receive - they are only valid in the future:

I would need to understand your use-case to comment on that. What I can say is that having this amount of items is bad design and will not be implemented in the binding. Most likely this was “invented” to work around the lack of timestamped states. In 2025 we should start receiving spot prices in intervals of 15 minutes. Then what - 192 items that will be rotated every 15 minutes? :slightly_smiling_face: I can’t even wrap my head around how to use that for calculations.

This pull request will probably pave some of the way:

I recently renamed all channels to prepare for supporting time series (e.g. from current-spot-price to just spot-price).

For now, instead of using the serialized JSON content, you could also simply use the thing action getPrices for retrieving all the prices in a rule. This will also perform much better.

Perhaps some of the thing actions could be helpful for some of your use-cases, for example if you need to calculate periods with lowest prices.

1 Like

Thanks a lot, it works after removing the channels :slight_smile:

My use-cases are:

  1. To show hourly prices in a table like what https://elspotpris.dk shows. What is would be the best way to show those prices with hourly or 3-hourly intervals?
  2. To calculate when cheapest to charge the car, but I think the action calculateCheapestPeriod you have implemented works much better. In my current rules I have a nested for loop to go hour by hour to find our when to start charging, and it won’t work when the price is updated every 15mins.

Best regards

Notice the timestamps are Instant, based on print out looks like it’s in GMT time:

  1. How can I check if Instant is today in local time
  2. and how to get local hour HH (2 digits) from Instant

Edit: found the issue please ignore the original post.

Did you also solve the problem of iterating the Map? If you could post your solution here, it could possibly help others finding this topic - and I might also be able to use it for improving the documentation.

EDIT: Okay, I just tested iterating - previously I only did key lookups in my rules:

val actions = getActions("energidataservice", "energidataservice:service:energidataservice");
var priceMap = actions.getPrices(null)
priceMap.forEach[ key, value | logInfo(key.toString, value.toString) ]

Hi, thanks for the reply!
The error message was misleading, it initially look like array out of bound. But it was because I only have one parameter when I logInfo, so iteration works perfectly fine. I’m now struggling to find out how to handle Instant, so far I have the following code (which is ugly I know), I will keep updating it:

    val actions = getActions("energidataservice", "energidataservice:service:energidataservice")
    var priceMap = actions.getPrices(null)
    var hourStart = now.toInstant().truncatedTo(ChronoUnit.HOURS)
    var String currentDate = new DateTimeType().format("%1$tY-%1$tm-%1$td")
    priceMap.forEach[ key, value | 
        logInfo("TEST", key.toString)
        var LocalDateTime ldt = LocalDateTime.ofInstant(key, ZoneOffset.systemDefault())
        var DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        var DateTimeFormatter hourFormatter = DateTimeFormatter.ofPattern("HH");
        var String date = ldt.format(dateFormatter)
        var String hour = ldt.format(hourFormatter)
        if (hour.length < 2){
            hour = "0" + hour
        }
       var String itemName = "ElectricityPriceAt" + hour
        if(date != currentDate) {
  			gEnergiDataServiceTomorrow.members.findFirst[name.equals(itemName+"Tomorrow")].postUpdate(value)
        }
        else {
            gEnergiDataServiceToday.members.findFirst[name.equals(itemName+"Today")].postUpdate(value)
        }
    ]

The idea is to find the item called ElectricityPriceAtHHToday/Tomorrow and update with the value of the map

1 Like