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…
2 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:

1 Like

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

1 Like