European electricity prices #
As of October 1, 2025, the electricity market in the European Union has transitioned to 15-minute intervals. We have introduced our implementation for the Home Energy Management System (HEMS) market to maximize the use of low-cost energy and generate savings for the end-user. Price monitoring is currently available for Latvia, Lithuania, Estonia, Finland, Sweden, Norway, Denmark and Germany.
Prices are EUR / MegaWattHour (EUR/MWh)
Data is acquired from elering.ee and energidataservice.dk
Further assistance can be found in this forum thread
Scheduled script #
Make that your script does not fetch data too quickly (we recommend once in 15 minutes)
Prices for Latvia, Lithuania, Estonia, and Finland #
country = 'lv' -- Latvia
--country = 'fi' -- Finland
--country = 'lt' -- Lithuania
--country = 'ee' -- Estonia
current_price_addr = '40/1/18'
cheapest_hour_addr = '40/1/16'
cheapest_hour_price_addr = '40/1/17'
costliest_hour_addr = '40/1/19'
cosltiest_hour_price_addr = '40/1/20'
cheapest_time_addr = '40/1/14'
cheapest_price_addr = '40/1/15'
costliest_time_addr = '40/1/21'
costliest_price_addr = '40/1/22'
current_date = os.date('*t')
local day_start_ts = os.time({year = current_date.year, month = current_date.month, day = current_date.day, hour = 0, min = 0, sec = 0})
local day_end_ts = os.time({year = current_date.year, month = current_date.month, day = current_date.day, hour = 23, min = 59, sec = 59})
local current_ts = os.time()
previous_date = os.date('%F', os.time() - 24*60*60)
next_date = os.date('%F', os.time() + 24*60*60)
url = 'https://dashboard.elering.ee/api/nps/price?start='.. previous_date ..'T00%3A00%3A00.999Z&end=' .. next_date ..'T00%3A00%3A00.999Z'
res, code = require('socket.http').request(url)
if res and code == 200 then
data = require('json').pdecode(res)
else
log('request error', res, code)
return
end
min_price = 99999
max_price = -1
min_price_timestamp = 0
max_price_timestamp = 0
current_price = nil
hourly_prices = {}
for h = 0, 23 do
hourly_prices[h] = { sum = 0, count = 0 }
end
if not data or not data.data or not data.data[country] then
log('No data for country: ' .. country)
return
end
for _, record in ipairs(data.data[country]) do
local timestamp = record.timestamp
local price = record.price
if timestamp >= day_start_ts and timestamp <= day_end_ts then
if price < min_price then
min_price = price
min_price_timestamp = timestamp
end
if price > max_price then
max_price = price
max_price_timestamp = timestamp
end
local record_time = os.date('*t', timestamp)
local hour = record_time.hour
if hourly_prices[hour] then
hourly_prices[hour].sum = hourly_prices[hour].sum + price
hourly_prices[hour].count = hourly_prices[hour].count + 1
end
end
if current_ts >= timestamp and current_ts < (timestamp + 15 * 60) then
current_price = price
end
end
--log('--- Current Status ---')
if current_price then
--log('Current 15-min price: ' .. current_price .. ' EUR/MWh')
grp.write(current_price_addr, current_price) -- float
else
log('Could not determine current price')
end
min_avg_price = 99999
max_avg_price = -1
cheapest_hour = -1
costliest_hour = -1
for hour, data in pairs(hourly_prices) do
if data.count > 0 then
local avg_price = data.sum / data.count
--log(string.format("Hour %02d:00 - Avg Price: %.2f", hour, avg_price))
if avg_price < min_avg_price then
min_avg_price = avg_price
cheapest_hour = hour
end
if avg_price > max_avg_price then
max_avg_price = avg_price
costliest_hour = hour
end
end
end
cheapest_hour_time = {}
cheapest_hour_time['wday'] = current_date['wday']
--log('--- Cheapest Hour Today ---')
if cheapest_hour ~= -1 then
--log(string.format('Hour: %02d:00 - %02d:59', cheapest_hour, cheapest_hour))
--log(string.format('Average Price: %.2f EUR/MWh', min_avg_price))
cheapest_hour_time['hour'] = cheapest_hour
grp.write(cheapest_hour_addr, cheapest_hour_time)
grp.write(cheapest_hour_price_addr, min_avg_price)
else
log('Could not calculate the cheapest hour.')
end
costliest_hour_time = {}
costliest_hour_time['wday'] = current_date['wday']
--log('--- Costliest Hour Today ---')
if costliest_hour ~= -1 then
--log(string.format('Hour: %02d:00 - %02d:59', costliest_hour, costliest_hour))
--log(string.format('Average Price: %.2f EUR/MWh', max_avg_price))
costliest_hour_time['hour'] = costliest_hour
grp.write(costliest_hour_addr, costliest_hour_time)
grp.write(cosltiest_hour_price_addr, max_avg_price)
else
log('Could not calculate the costliest hour.')
end
cheapest_time = {}
cheapest_time['wday'] = current_date['wday']
--log('--- Cheapest 15-min Interval Today ---')
if min_price_timestamp > 0 then
local min_price_time = os.date('*t', min_price_timestamp)
--log('Price: ' .. min_price .. ' EUR/MWh')
--log('Time: ' .. string.format('%02d:%02d', min_price_time.hour, min_price_time.min))
cheapest_time['hour'] = min_price_time.hour
cheapest_time['minute'] = min_price_time.min
grp.write(cheapest_time_addr, cheapest_time)
grp.write(cheapest_price_addr, min_price)
else
log('Could not find minimum price for today')
end
costliest_time = {}
costliest_time['wday'] = current_date['wday']
--log('--- Costliest 15-min Interval Today ---')
if max_price_timestamp > 0 then
local max_price_time = os.date('*t', max_price_timestamp)
--log('Price: ' .. max_price .. ' EUR/MWh')
--log('Time: ' .. string.format('%02d:%02d', max_price_time.hour, max_price_time.min))
costliest_time['hour'] = max_price_time.hour
costliest_time['minute'] = max_price_time.min
grp.write(costliest_time_addr, costliest_time)
grp.write(costliest_price_addr, max_price)
else
log('Could not find maximum price for today')
end
Prices for Denmark, Sweden, Norway, and Germany #
price_area = 'DK1' -- west Denmark DK1 grid region
--price_area = 'DK2' -- east Denmark DK2 grid region
--price_area = 'DE' -- Germany
--price_area = 'NO2' -- southern and southwestern Norway
--price_area = 'SE3' -- north Sweden
--price_area = 'SE4' -- south Sweden
current_price_addr = '40/1/18' -- 14. 4 byte floating point
cheapest_hour_addr = '40/1/16' -- 10. 3 byte time / day
cheapest_hour_price_addr = '40/1/17' -- 14. 4 byte floating point
costliest_hour_addr = '40/1/19' -- 10. 3 byte time / day
costliest_hour_price_addr = '40/1/20' -- 14. 4 byte floating point
start_date = os.date('%Y-%m-%d')
end_date = os.date('%Y-%m-%d', os.time() + 48 * 60 * 60)
url = 'https://api.energidataservice.dk/dataset/DayAheadPrices?start=' ..
start_date .. '&end=' .. end_date ..
'&filter={"PriceArea":["' .. price_area .. '"]}'
resp, code = require('socket.http').request(url)
if resp and code == 200 then
data = require('json').pdecode(resp)
else
log('request error', resp, code)
return
end
if not data or not data.records then
log('No data for price area: ' .. price_area)
return
end
curr_day = tonumber(os.date('%u'))
next_day = curr_day == 7 and 1 or (curr_day + 1)
curr_date = os.date('%Y%m%d')
curr_hour_str = curr_date .. os.date('%H')
curr_hour = tonumber(curr_hour_str)
curr_min = math.floor(tonumber(os.date('%M')) / 15) * 15
curr_ts = curr_hour_str .. string.format('%02d', curr_min)
prices = {}
for _, record in ipairs(data.records) do
ts = record.TimeDK:gsub('%-', ''):gsub('T', ''):gsub(':', '')
hour = tonumber(ts:sub(1, 10))
price = record.DayAheadPriceEUR
if hour >= curr_hour then
store = prices[ hour ] or { sum = 0, count = 0 }
store.sum = store.sum + price
store.count = store.count + 1
prices[ hour ] = store
end
if ts:sub(1, 12) == curr_ts then
curr_price = price
end
end
hourly = {}
for hour, store in pairs(prices) do
price = store.sum / store.count
hourly[ #hourly + 1 ] = {
hour = hour,
price = price
}
end
table.sort(hourly, function(a, b)
return a.price > b.price
end)
function update(store, hour_addr, price_addr)
local hour = tostring(store.hour)
local date = hour:sub(1, 8)
local day = date == curr_date and curr_day or next_day
grp.checkupdate(hour_addr, { day = day, hour = hour:sub(9, 10) })
grp.checkupdate(price_addr, store.price)
end
grp.checkupdate(current_price_addr, curr_price)
update(hourly[1], costliest_hour_addr, costliest_hour_price_addr)
update(hourly[#hourly], cheapest_hour_addr, cheapest_hour_price_addr)