require 'open-uri' EASY_ENERGY_TARIFFS = {} class Cost attr_reader :battery, :max_charge_kwh, :entsoe def initialize(battery_capacity=10.0, max_charge=5.0) @entsoe = Entsoe.new @max_charge_kwh = max_charge @battery = Battery.new(battery_capacity) end def format_cost(cost) cost ? "EUR %0.03f" % cost : "EUR ?" end def add_tax(formatted_hour,usage_kwh,usage_kwh_cost,return_kwh, return_kwh_cost) return nil if (usage_kwh.nil? || usage_kwh_cost.nil? || return_kwh.nil? || return_kwh_cost.nil?) year = Date.parse(formatted_hour).year case year when 2020 usage_kwh * (usage_kwh_cost + 0.11822 + 0.03303) - return_kwh * (return_kwh_cost + 0.11822 + 0.03303) when 2021 # see https://www.vastelastenbond.nl/blog/overzicht-energiebelasting-en-ode-2021-2022-en-je-energierekening-2021/ usage_kwh * (usage_kwh_cost + 0.11408 + 0.03630) - return_kwh * (return_kwh_cost + 0.11408 + 0.03630) when 2022 usage_kwh * (usage_kwh_cost + 0.04452 + 0.03691) - return_kwh * (return_kwh_cost + 0.04452 + 0.03691) end end ###################################################### # Easy Energy API (proprietary) - better to use entsoe ###################################################### # returns a hash with keys formatted "yyyy-mm-dd-hr" and values [usage, return] # e.g. { 2021-12-16-06"=>[0.36058, 0.298] } def easy_energy_tariffs(date) p "Fetching EasyEnergy tariffs for %s" % date.strftime("%F-%H") # calculate offset (date is in UTC, and we want to have tariffs in Amsterdam zone) zone = 'Amsterdam' offset = DateTime.now.in_time_zone(zone).utc_offset date = date.beginning_of_day.advance(seconds: offset) url = "https://mijn.easyenergy.com/nl/api/tariff/getapxtariffs?startTimestamp=%s&endTimestamp=%s&grouping=" % [date.strftime("%F %T"),date.advance(:hours => 24).strftime("%F %T")] #p urlvandaag json = JSON.load(URI::open(url)) # advancing with 1 hrs (to offset for something?) json.map{|t| [DateTime.parse(t["Timestamp"]).strftime("%F-%H"), [t["TariffUsage"], t["TariffReturn"]]]}.to_h end def easy_energy_rate(formatted_hour) unless EASY_ENERGY_TARIFFS.key?(formatted_hour) EASY_ENERGY_TARIFFS.merge!(easy_energy_tariffs(Date.parse(formatted_hour))) end EASY_ENERGY_TARIFFS[formatted_hour] 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 = easy_energy_rate(formatted_hour) add_tax(formatted_hour, usage_kwh, usage_kwh_cost, return_kwh, return_kwh_cost) end def easy_energy_high_low(date) hour_start = date.beginning_of_day day_end = hour_start.advance(days: 1) max_rate = 0.0 min_rate = 100.0 min_hour = nil max_hour = nil while(hour_start < day_end) do easy_usage_rate, easy_return_rate = easy_energy_rate(hour_start.strftime("%F-%H")) unless easy_usage_rate.nil? # determine max if easy_usage_rate > max_rate max_rate = easy_usage_rate max_hour = hour_start.strftime("%F-%H") end # determine min if easy_usage_rate < min_rate min_rate = easy_usage_rate min_hour = hour_start.strftime("%F-%H") end end hour_start = hour_start.advance(:hours => 1) end return min_hour,max_hour 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 end end def oxxio_energy_cost(formatted_hour, normaal_kwh, dal_kwh, year_shift=0) return nil if (normaal_kwh.nil? || dal_kwh.nil?) year = Date.parse(formatted_hour).year+year_shift case year when 2020 normaal_kwh * (0.07865 + 0.11822 + 0.03303) + dal_kwh * (0.06215 + 0.11822 + 0.03303) when 2021 normaal_kwh * (0.06782 + 0.11408 + 0.03630) + dal_kwh * (0.05259 + 0.11408 + 0.03630) when 2022 normaal_kwh * (0.23665 + 0.04452 + 0.03691) + dal_kwh * (0.19408 + 0.04452 + 0.03691) end 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) 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.beginning_of_day day_end = hour_start.advance(days: 1) result = [] lowest_hour, highest_hour,high_hours = entsoe.high_low_hours(date) while(hour_start < day_end) do hour_end = hour_start.end_of_hour formatted_hour = hour_start.strftime("%F-%H") #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 entsoe_cost = entsoe_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 low_hour.eql?(formatted_hour) # charge battery during lowest hour usage_kwh += battery.charge(max_charge_kwh) else # if during expensive hours || more than kwh then discharge_battery if (battery.battery_kwh > max_charge_kwh) || high_hours.include?(formatted_hour) usage_kwh -= battery.discharge(usage_kwh) end end end entsoe_cost_with_battery = entsoe_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 entsoe_cost_with_battery, entsoe_cost, oxxio_rate, oxxio_cost] p "%s,%s,%s,%s,%s,%s,%s,%s,%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