diff --git a/community/electricity-price-widget.lua b/community/electricity-price-widget.lua new file mode 100644 index 0000000..a3f3c83 --- /dev/null +++ b/community/electricity-price-widget.lua @@ -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 " +-- 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("⋄ %s′ %s ", get_display_time(i), get_display_price(i)) + cur_price = price(i) + if more_count > 3 then + break + end + end + end + return string.format("%s %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