Add Electricity spot price widget

This widget supports showing electricity spot price for many areas in
Europe. However, there's no configuration UI yet. I'm hoping there will
be native support for editing the values in `prefs` later, but I'm also
open to implementing the configuration UI in the widget if I get some
help.

The chart would be better if displayed with timestamps, but that doesn't
currently work when the timestamps span two days.
This commit is contained in:
Hannu Hartikainen
2024-05-06 09:38:29 +03:00
parent 36f4e4856b
commit b83fb1e1bb

View File

@@ -0,0 +1,183 @@
-- name = "Electricity spot price"
-- description = "Day-ahead spot price of electricity"
-- data_source = "https://api.energy-charts.info/"
-- type = "widget"
-- foldable = "true"
-- author = "Hannu Hartikainen <hannu.hartikainen@gmail.com>"
-- version = "1.0"
json = require "json"
prefs = require "prefs"
url_base = "https://api.energy-charts.info/price?bzn=%s&end=%d"
price_data = nil
price_interval = nil
price_unit = nil
unit_multiplier = 1
next_fetch_ts = 0
next_redraw_ts = nil
-- parameters and default values
function on_load()
-- bidding zone: see "Available bidding zones" in https://api.energy-charts.info/
if not prefs.bidding_zone then
prefs.bidding_zone = "FI"
end
-- VAT percentage
if not prefs.vat_percentage then
prefs.vat_percentage = 24
end
-- change threshold percentages for showing changes in folded format
if not prefs.oneline_threshold_high then
prefs.oneline_threshold_high = 1.3
end
if not prefs.oneline_threshold_low then
prefs.oneline_threshold_low = 0.7
end
-- url to open when clicked
if not prefs.click_url then
prefs.click_url = "https://www.sahkonhintatanaan.fi/"
end
end
function get_url()
-- at most about 35 hours are known in advance; fetch all known prices
local end_offset = 2*24*60*60
local end_ts = os.time() + end_offset
return string.format(url_base, prefs.bidding_zone, end_ts)
end
function on_alarm()
if os.time() > next_fetch_ts then
http:get(get_url())
end
end
function on_network_result(result, code)
if code >= 200 and code < 299 then
parse_result(result)
draw_widget(true)
end
end
function on_tick(t)
local ts = os.time()
if next_redraw_ts and ts > next_redraw_ts then
draw_widget(true)
end
end
function on_click()
if not ui:folding_flag() then
system:open_browser(prefs.click_url)
else
draw_widget(false)
end
end
function parse_result(result)
price_data = json.decode(result)
local price_count = #price_data.unix_seconds
if price_count < 2 then
return
end
price_interval = price_data.unix_seconds[2] - price_data.unix_seconds[1]
if price_data.unit == "EUR/MWh" then
price_unit = "c/kWh"
unit_multiplier = 0.1
else
price_unit = price_data.unit
unit_multiplier = 1
end
-- assume next day is known 8 hours before it starts
-- (eg. Nord Pool Spot typically publishes dayahead prices at 14 local time)
local next_fetch_offset = 8*60*60
local end_of_data_ts = price_data.unix_seconds[price_count] + price_interval
next_fetch_ts = end_of_data_ts - next_fetch_offset
end
function get_current_idx()
local t = os.time()
for i = 1, #price_data.unix_seconds do
local ts = price_data.unix_seconds[i]
if ts < t and t < (ts+price_interval) then
return i
end
end
end
function price(i)
return price_data.price[i]
end
function get_display_price(idx)
local mul = (1.0 + (prefs.vat_percentage / 100.0)) * unit_multiplier
-- NOTE: float rounding in string.format doesn't work so do it here
return math.floor(100.0 * price(idx) * mul + 0.5) / 100.0
end
function get_display_time(idx)
return os.date("%H", price_data.unix_seconds[idx])
end
function format_price(idx)
return string.format("%0.2f", get_display_price(idx))
end
function format_price_and_unit(idx)
return string.format("%s %s", format_price(idx), price_unit)
end
function format_oneline(idx)
local more_prices = ""
local more_count = 0
local cur_price = price(idx)
for i = idx+1, #price_data.price do
if price(i) > prefs.oneline_threshold_high * cur_price
or price(i) < prefs.oneline_threshold_low * cur_price then
more_count = more_count + 1
more_prices = more_prices .. string.format("⋄ <i>%s</i> <b>%s</b> ", get_display_time(i), get_display_price(i))
cur_price = price(i)
if more_count > 3 then
break
end
end
end
return string.format("<b>%s</b> %s", format_price_and_unit(idx), more_prices)
end
-- NOTE: using timestamps would be better than indices, but the chart element
-- doesn't support times spanning multiple days properly
function make_chart_data(idx)
local chart = {}
for i = idx, #price_data.price do
table.insert(chart, {
i-1,
get_display_price(i)
})
end
return chart
end
function draw_widget(fold)
ui:set_folding_flag(fold)
local idx = get_current_idx()
if not idx then
ui:show_text("Error: no current price data")
-- request fetch on next on_alarm and don't redraw before that
next_fetch_ts = 0
next_redraw_ts = nil
return
end
next_redraw_ts = price_data.unix_seconds[idx+1]
ui:set_title(string.format("Electricity spot price: %s", format_price_and_unit(idx)))
ui:show_chart(make_chart_data(idx), "x: int, y: float", "", true, format_oneline(idx))
end