454 lines
16 KiB
Ruby
454 lines
16 KiB
Ruby
require 'open-uri'
|
|
require 'i18n'
|
|
require 'gruff'
|
|
|
|
EASY_ENERGY_TARIFFS = {}
|
|
|
|
# See https://www.belastingdienst.nl/wps/wcm/connect/bldcontentnl/belastingdienst/zakelijk/overige_belastingen/belastingen_op_milieugrondslag/tarieven_milieubelastingen/tabellen_tarieven_milieubelastingen
|
|
# Without VAT
|
|
ENERGY_TAX_KWH = { 2020 => 0.09770, 2021 => 0.09428, 2022 => 0.03679, 2023 => 0.12599, 2024 => 0.10880, 2025 => 0.10154}
|
|
ODE_KWH = { 2020 => 0.0273, 2021 => 0.0300, 2022 => 0.0305, 2023 => 0.0, 2024 => 0.0, 2025 => 0.0}
|
|
# merge by adding values
|
|
TAX_KWH = ENERGY_TAX_KWH.merge(ODE_KWH){|key, energy_tax, ode| energy_tax + ode}
|
|
|
|
|
|
class Cost
|
|
|
|
attr_accessor :max_charge_kwh
|
|
attr_reader :battery, :entsoe, :zone
|
|
|
|
def initialize(zone="Amsterdam", battery_capacity=10.0, max_charge=5.0, storage_cost=0.05)
|
|
@zone = zone
|
|
@entsoe = Entsoe.new(zone, storage_cost)
|
|
@max_charge_kwh = max_charge
|
|
@battery = Battery.new(battery_capacity)
|
|
|
|
# Allow for Dutch titles in graphs
|
|
I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"]
|
|
I18n.available_locales = [:en, :nl]
|
|
I18n.locale = :nl
|
|
end
|
|
|
|
# Government reduced VAT to 9% from 1 July 2022 until 31 Dec 2022
|
|
def vat_at(date)
|
|
jul22 = Date.parse("2022-09-01")
|
|
dec22 = Date.parse("2022-12-31")
|
|
if (date >= jul22 && date <= dec22)
|
|
0.09
|
|
else
|
|
0.21
|
|
end
|
|
end
|
|
|
|
def format_cost(cost)
|
|
cost ? "EUR %0.03f" % cost : "EUR ?"
|
|
end
|
|
|
|
# Assume: usage_kwh_cost, return_kwh_cost already includes vat
|
|
# calling this with only 'usage_kwh' parameter returns the tax amount for those kwh only.
|
|
#
|
|
def add_tax(formatted_hour,usage_kwh,usage_kwh_cost=0.0,return_kwh=0.0, return_kwh_cost=0.0)
|
|
return nil if (usage_kwh.nil? || usage_kwh_cost.nil? || return_kwh.nil? || return_kwh_cost.nil?)
|
|
|
|
vat = 1 + vat_at(Date.parse(formatted_hour))
|
|
year = Date.parse(formatted_hour).year
|
|
# calculate tax per kwh - which the sum of energy_tax and ode
|
|
tax = TAX_KWH[year]*vat
|
|
|
|
usage_kwh * (usage_kwh_cost + tax) - return_kwh * (return_kwh_cost + tax)
|
|
|
|
end
|
|
|
|
######################################################
|
|
# Easy Energy - makes use of entsoe, adds EasyEnergy opslag
|
|
######################################################
|
|
|
|
def easy_energy_rate(formatted_hour)
|
|
year = Date.parse(formatted_hour).year
|
|
month = Date.parse(formatted_hour).month
|
|
|
|
case year
|
|
when 2020..2021
|
|
# opslag, zonder BTW
|
|
0.00800
|
|
when 2022
|
|
case month
|
|
when 1..11
|
|
# opslag, before increase
|
|
0.00800
|
|
when 12
|
|
# opslag, met BTW: 0,01962
|
|
0.018
|
|
end
|
|
when 2023
|
|
0.018
|
|
when 2024
|
|
# opslag met BTW: 0,02178
|
|
0.018457
|
|
end
|
|
end
|
|
|
|
def easy_energy_cost(formatted_hour, usage_kwh, return_kwh)
|
|
return nil if (usage_kwh.nil? || return_kwh.nil?)
|
|
|
|
usage_kwh_cost = return_kwh_cost = (entsoe.price_at(formatted_hour)+easy_energy_rate(formatted_hour))*(1+vat_at(Date.parse(formatted_hour)))
|
|
add_tax(formatted_hour, usage_kwh, usage_kwh_cost, return_kwh, return_kwh_cost)
|
|
end
|
|
|
|
def easy_energy_hours(date)
|
|
hour_start = date.in_time_zone(zone).beginning_of_day
|
|
day_end = hour_start.advance(days: 1)
|
|
result = []
|
|
while(hour_start < day_end) do
|
|
formatted_hour = hour_start.strftime("%F %H")
|
|
result << easy_energy_cost(formatted_hour, 1, 0)
|
|
hour_start = hour_start.advance(:hours => 1)
|
|
end
|
|
result
|
|
end
|
|
|
|
def easy_energy_tariff_barplot(date)
|
|
hours = (0..23).to_a
|
|
costs = easy_energy_hours(date)
|
|
g = Gruff::Bar.new()
|
|
g.title = "Tarief per kwH (incl. belastingen en BTW) - %s" % I18n.localize(date, format: "%A, %e %B %Y")
|
|
g.x_axis_label = "uur"
|
|
#g.y_axis_label = "EUR"
|
|
g.y_axis_label_format = lambda do |value|
|
|
"€ %.2f" % value
|
|
end
|
|
g.labels = hours
|
|
g.data :costs, costs
|
|
g.write("plots/easy_tariff_%s.png" % date.strftime("%F"))
|
|
end
|
|
|
|
# calculate the hourly cost (raw!) between start_hour and end_hour
|
|
# raw means: entsoe prices + VAT
|
|
def easy_energy_raw_hourly_cost_between(start_hour, end_hour)
|
|
begin_hour = start_hour
|
|
costs = []
|
|
while(begin_hour < end_hour) do
|
|
# get usage_kwh/return_kwh for one hour
|
|
begin_hour_plus1 = begin_hour.end_of_hour
|
|
hour_readings = Reading.where("created_at > :begin AND created_at < :end", {:begin => begin_hour, :end => begin_hour_plus1})
|
|
hour_diff = hour_readings.last ? hour_readings.last.diff(hour_readings.first) : UNKNOWN_READING
|
|
# helper to remove distiction between low/high tarif consumption
|
|
usage_kwh, return_kwh = Reading.consumed_and_produced_for_diff(hour_diff)
|
|
|
|
formatted_hour = begin_hour.strftime("%F %H")
|
|
# calculate RAW price (entsoe price + VAT)
|
|
kwh_price = entsoe.price_at(formatted_hour)*(1+vat_at(Date.parse(formatted_hour)))
|
|
hour_cost = (usage_kwh.nil? || return_kwh.nil? || kwh_price.nil?) ? nil : ((usage_kwh-return_kwh)*kwh_price)
|
|
costs << hour_cost
|
|
|
|
# do the next hour
|
|
begin_hour = begin_hour.advance(:hours => 1)
|
|
end
|
|
# give the result
|
|
costs
|
|
end
|
|
|
|
# calculate the hourly cost between start_hour and end_hour
|
|
def easy_energy_hourly_cost_between(start_hour, end_hour)
|
|
begin_hour = start_hour
|
|
costs = []
|
|
while(begin_hour < end_hour) do
|
|
# get usage_kwh/return_kwh for one hour
|
|
begin_hour_plus1 = begin_hour.end_of_hour
|
|
hour_readings = Reading.where("created_at > :begin AND created_at < :end", {:begin => begin_hour, :end => begin_hour_plus1})
|
|
hour_diff = hour_readings.last ? hour_readings.last.diff(hour_readings.first) : UNKNOWN_READING
|
|
# helper to remove distiction between low/high tarif consumption
|
|
usage_kwh, return_kwh = Reading.consumed_and_produced_for_diff(hour_diff)
|
|
|
|
formatted_hour = begin_hour.strftime("%F %H")
|
|
costs << easy_energy_cost(formatted_hour, usage_kwh, return_kwh)
|
|
# do the next hour
|
|
begin_hour = begin_hour.advance(:hours => 1)
|
|
end
|
|
# give the result
|
|
costs
|
|
end
|
|
|
|
def easy_energy_daily_cost_between(start_day, end_day)
|
|
curr_day = start_day
|
|
costs = []
|
|
while (curr_day <= end_day) do
|
|
hour_start = curr_day.in_time_zone(zone).beginning_of_day
|
|
hour_end = hour_start.advance(days: 1)
|
|
|
|
costs_24hours = easy_energy_hourly_cost_between(hour_start,hour_end)
|
|
if costs_24hours.any?{ |e| e.nil? }
|
|
p "Not all Reading data between %s and %s is available!" % [I18n.localize(start_day, format: "%e %B %Y"), I18n.localize(end_day, format: "%e %B %Y")]
|
|
end
|
|
# add the sum of 24 hours
|
|
costs << costs_24hours.compact.sum
|
|
# do the next day
|
|
curr_day = curr_day.advance(:days => 1)
|
|
end
|
|
# give the result
|
|
costs
|
|
end
|
|
|
|
# calculate leveringskosten (entsoe prices + VAT) in one month
|
|
# date = arbitrary day for the month
|
|
def easy_energy_raw_cost_in_month(date)
|
|
hour_start = date.beginning_of_month.in_time_zone(zone).beginning_of_day
|
|
hour_end = date.end_of_month.in_time_zone(zone).end_of_day
|
|
# can't get cost for the future
|
|
hour_end = Time.now if hour_end > Time.now
|
|
|
|
easy_energy_raw_hourly_cost_between(hour_start,hour_end)
|
|
end
|
|
|
|
def easy_energy_cost_in_month(date)
|
|
hour_start = date.beginning_of_month.in_time_zone(zone).beginning_of_day
|
|
hour_end = date.end_of_month.in_time_zone(zone).end_of_day
|
|
# can't get cost for the future
|
|
hour_end = Time.now if hour_end > Time.now
|
|
|
|
easy_energy_hourly_cost_between(hour_start,hour_end)
|
|
end
|
|
|
|
def easy_energy_hourly_cost_for(date)
|
|
hour_start = date.in_time_zone(zone).beginning_of_day
|
|
day_end = hour_start.advance(days: 1)
|
|
easy_energy_hourly_cost_between(hour_start, day_end)
|
|
end
|
|
|
|
def easy_energy_cost_barplot(date)
|
|
# get array with 24 hourly cost values
|
|
costs = easy_energy_hourly_cost_for(date)
|
|
|
|
# create plot
|
|
hours = (0..23).to_a
|
|
g = Gruff::Bar.new
|
|
g.title = "Verbruikskosten (incl. belastingen en BTW) - %s" % I18n.localize(date, format: "%A, %e %B %Y")
|
|
g.x_axis_label = "uur"
|
|
#g.y_axis_label = "EUR"
|
|
g.y_axis_label_format = lambda do |value|
|
|
"€ %.2f" % value
|
|
end
|
|
g.labels = hours
|
|
g.data :costs, costs
|
|
g.write("plots/easy_cost_%s.png" % date.strftime("%F"))
|
|
|
|
# return the sum cost
|
|
costs.sum
|
|
end
|
|
|
|
|
|
######################################################
|
|
# Oxxio rates and cost
|
|
######################################################
|
|
|
|
def oxxio_rate(formatted_hour, high_tariff)
|
|
year = Date.parse(formatted_hour).year
|
|
case year
|
|
when 2020
|
|
high_tariff ? 0.07865 : 0.06215
|
|
when 2021
|
|
high_tariff ? 0.06782 : 0.05259
|
|
when 2022
|
|
high_tariff ? 0.23665 : 0.19408
|
|
when 2023
|
|
# rate excl. VAT
|
|
high_tariff ? 0.47758 : 0.34165
|
|
when 2024
|
|
0.25767769
|
|
when 2025
|
|
high_tariff ? 0.2695 : 0.2296
|
|
end
|
|
end
|
|
|
|
# Optional: raw! (tarif + VAT) cost
|
|
def oxxio_energy_cost(formatted_hour, normaal_kwh, dal_kwh, with_tax=true, year_shift=0)
|
|
return nil if (normaal_kwh.nil? || dal_kwh.nil?)
|
|
#year = Date.parse(formatted_hour).year+year_shift
|
|
date = Date.parse(formatted_hour).advance(years: year_shift)
|
|
case date.to_time.to_i
|
|
# Date.parse("2019-12-08").to_time.to_i
|
|
# From 8 Dec 2019 until 7 Dec 2020
|
|
when 1575763200..1607385599
|
|
#normaal_kwh * (0.07865 + 0.11822 + 0.03303) + dal_kwh * (0.06215 + 0.11822 + 0.03303)
|
|
normaal_kwh_cost = 0.07865
|
|
dal_kwh_cost = 0.06215
|
|
# From 8 Dec 2020 until 7 Dec 2021
|
|
when 1607385600..1638921599
|
|
#normaal_kwh * (0.06782 + 0.11408 + 0.03630) + dal_kwh * (0.05259 + 0.11408 + 0.03630)
|
|
normaal_kwh_cost = 0.06782
|
|
dal_kwh_cost = 0.05259
|
|
# From 8 Dec 2021 until 7 Sept 2022
|
|
when 1638921600..1662595199
|
|
# normaal_kwh * (0.23665 + 0.04452 + 0.03691) + dal_kwh * (0.19408 + 0.04452 + 0.03691)
|
|
normaal_kwh_cost = 0.23665
|
|
dal_kwh_cost = 0.19408
|
|
# From 8 Sept 2022 until 31 December 2022
|
|
when 1662595200..1672527599
|
|
normaal_kwh_cost = 0.60824
|
|
dal_kwh_cost = 0.43701
|
|
# From 1 Jan 2023 until 31 December 2023
|
|
when 1672527600..1704063599
|
|
vat = 1 + vat_at(Date.parse(formatted_hour))
|
|
normaal_kwh_cost = 0.47758*vat
|
|
dal_kwh_cost = 0.34165*vat
|
|
# From 1 Jan 2024 until 31 December 2024
|
|
when 1704063600..1735603199
|
|
vat = 1 + vat_at(Date.parse(formatted_hour))
|
|
normaal_kwh_cost = 0.25767769*vat
|
|
dal_kwh_cost = 0.25767769*vat
|
|
when 1735711200..1767160800
|
|
vat = 1 + vat_at(Date.parse(formatted_hour))
|
|
normaal_kwh_cost = 0.2695*vat
|
|
dal_kwh_cost = 0.2296*vat
|
|
else
|
|
p "Not supported interval Oxxio for value: %d" % date.to_time.to_i
|
|
# catch-all, incase 'formated_hour' is outside any of the cases
|
|
normaal_kwh_cost = 0.0
|
|
dal_kwh_cost = 0.0
|
|
end
|
|
|
|
if with_tax
|
|
normaal_cost = add_tax(formatted_hour, normaal_kwh,normaal_kwh_cost) # return_kwh already accounted for
|
|
dal_cost = add_tax(formatted_hour, dal_kwh, dal_kwh_cost)
|
|
else
|
|
normaal_cost = normaal_kwh*normaal_kwh_cost
|
|
dal_cost = dal_kwh*dal_kwh_cost
|
|
end
|
|
|
|
# result
|
|
return normaal_cost, dal_cost
|
|
end
|
|
|
|
#
|
|
# Entsoe
|
|
#
|
|
|
|
def entsoe_energy_cost(formatted_hour, usage_kwh, return_kwh)
|
|
return nil if (usage_kwh.nil? || return_kwh.nil?)
|
|
|
|
usage_kwh_cost = return_kwh_cost = entsoe.price_at(formatted_hour)*(1+vat_at(Date.parse(formatted_hour)))
|
|
add_tax(formatted_hour, usage_kwh, usage_kwh_cost, return_kwh, return_kwh_cost)
|
|
end
|
|
|
|
######################################################
|
|
# Calculate the per_hour usage and costs
|
|
######################################################
|
|
|
|
def hours(date, year_shift=0)
|
|
hour_start = date.in_time_zone(zone).beginning_of_day
|
|
day_end = hour_start.advance(days: 1)
|
|
result = []
|
|
lowest_hour, highest_hour,high_hours = entsoe.high_low_hours(date.advance(years: year_shift))
|
|
|
|
while(hour_start < day_end) do
|
|
hour_end = hour_start.end_of_hour
|
|
|
|
#p "Fetching meter readings between %s and %s" % [hour_start,hour_end]
|
|
hour_readings = Reading.where("created_at > :begin AND created_at < :end", {:begin => hour_start, :end => hour_end})
|
|
hour_diff = hour_readings.last ? hour_readings.last.diff(hour_readings.first) : UNKNOWN_READING
|
|
# calculate cost of this hour
|
|
usage_kwh = hour_diff[:total_kwh_consumed_high] + hour_diff[:total_kwh_consumed_low] rescue nil
|
|
return_kwh = hour_diff[:total_kwh_produced_high] + hour_diff[:total_kwh_produced_low] rescue nil
|
|
|
|
formatted_hour = hour_start.advance(years: year_shift).strftime("%F %H")
|
|
easy_cost = easy_energy_cost(formatted_hour, usage_kwh, return_kwh) # without battery use
|
|
|
|
#
|
|
# Make battery work
|
|
#
|
|
if !usage_kwh.nil?
|
|
# charge battery with return_kwh
|
|
return_kwh -= battery.charge(return_kwh)
|
|
|
|
if (lowest_hour.eql?(formatted_hour) || entsoe.price_at(formatted_hour) < 0) # lowest_hour = "" if small difference between high/low
|
|
# charge battery during lowest hour, or when prices are negative
|
|
usage_kwh += battery.charge(max_charge_kwh)
|
|
else
|
|
# if during expensive hours || more than <max_charge_kwh> kwh then discharge_battery
|
|
# if (battery.battery_kwh > max_charge_kwh) || high_hours.include?(formatted_hour)
|
|
if high_hours.include?(formatted_hour)
|
|
usage_kwh -= battery.discharge(usage_kwh)
|
|
end
|
|
end
|
|
end
|
|
easy_cost_with_battery = easy_energy_cost(formatted_hour, usage_kwh, return_kwh) # with battery use
|
|
|
|
#
|
|
# end of battery work
|
|
#
|
|
|
|
normal_usage_kwh = hour_diff[:total_kwh_consumed_low] - hour_diff[:total_kwh_produced_low] rescue nil
|
|
low_usage_kwh = hour_diff[:total_kwh_consumed_high] - hour_diff[:total_kwh_produced_high] rescue nil
|
|
oxxio_cost = oxxio_energy_cost(formatted_hour,normal_usage_kwh, low_usage_kwh, year_shift)
|
|
oxxio_rate = oxxio_rate(formatted_hour, (hour_readings.first.high_tarif rescue true))
|
|
|
|
one_hour = [formatted_hour,
|
|
hour_diff[:total_kwh_consumed_high],
|
|
hour_diff[:total_kwh_consumed_low],
|
|
hour_diff[:total_kwh_produced_high],
|
|
hour_diff[:total_kwh_produced_low],
|
|
usage_kwh,
|
|
return_kwh,
|
|
battery.battery_kwh,
|
|
entsoe.price_at(formatted_hour), # entsoe rate
|
|
easy_cost_with_battery,
|
|
easy_cost,
|
|
oxxio_rate,
|
|
oxxio_cost]
|
|
|
|
p "%s,%s,%s,%s,%s,%s,%s,%0.1f,%s,%s,%s,%s,%s" % (one_hour[0..7] + one_hour[8..12].map{|c| format_cost(c)})
|
|
result << one_hour
|
|
|
|
hour_start = hour_start.advance(:hours => 1)
|
|
end
|
|
result
|
|
end
|
|
|
|
def summarize_hours(hours)
|
|
usage_kwh = hours.map{|e| (e[1] && e[2]) ? (e[1] + e[2]) : nil}.compact.sum
|
|
return_kwh = hours.map{|e| (e[3] && e[4]) ? (e[3] + e[4]) : nil}.compact.sum
|
|
usage_kwh_with_battery = hours.map{|e| e[5]}.compact.sum
|
|
return_kwh_with_battery = hours.map{|e| e[6]}.compact.sum
|
|
entsoe_cost_with_battery = hours.map{|e| e[9]}.compact.sum
|
|
entsoe_cost = hours.map{|e| e[10]}.compact.sum
|
|
oxxio_cost = hours.map{|e| e[12]}.compact.sum
|
|
|
|
return usage_kwh, return_kwh, usage_kwh_with_battery, return_kwh_with_battery, format_cost(entsoe_cost_with_battery), format_cost(entsoe_cost), format_cost(oxxio_cost)
|
|
end
|
|
|
|
def hours_in_month(date, year_shift=0)
|
|
day = date.beginning_of_month
|
|
last_day = date.end_of_month
|
|
all_hours = []
|
|
while (day <= last_day)
|
|
all_hours += hours(day, year_shift)
|
|
day = day.advance(:days => 1)
|
|
end
|
|
all_hours
|
|
end
|
|
|
|
# heel2021 = hours_in(Date.parse("2021-01-01"), Date.parse("2021-12-31"), 1)
|
|
|
|
def hours_in(from,to, year_shift=0)
|
|
day = from.to_date
|
|
last_day = to.to_date
|
|
all_hours = []
|
|
while (day <= last_day)
|
|
all_hours += hours(day, year_shift)
|
|
day = day.advance(:days => 1)
|
|
end
|
|
all_hours
|
|
end
|
|
|
|
def write_csv(filename,hours)
|
|
CSV.open(filename, "wb") do |csv|
|
|
csv << ["hour", "kwh_consumed_low", "kwh_consumed_high", "kwh_produced_low", "kwh_produced_high", "easy_usage_rate", "easy_return_rate", "easy_cost", "oxxio_rate", "oxxio_cost"]
|
|
hours.each do |row|
|
|
csv << row
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
end
|