Cost calculations
This commit is contained in:
@@ -1 +1 @@
|
||||
ruby-2.4.3
|
||||
ruby-2.7
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM ruby:2.4.3
|
||||
FROM ruby:2.7
|
||||
|
||||
ENV BUILD_PACKAGES="apt-utils build-essential curl less nodejs sudo wget zsh libmysqlclient-dev libserialport-dev cron"
|
||||
ENV BUILD_PACKAGES="apt-utils build-essential curl less nodejs sudo wget zsh libmariadb-dev libserialport-dev cron"
|
||||
|
||||
# throw errors if Gemfile has been modified since Gemfile.lock
|
||||
RUN \
|
||||
@@ -10,6 +10,7 @@ RUN \
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
|
||||
RUN \
|
||||
apt-get update -qq && \
|
||||
apt-get install -y $BUILD_PACKAGES && \
|
||||
|
||||
53
Gemfile.lock
53
Gemfile.lock
@@ -1,36 +1,37 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activemodel (5.1.5)
|
||||
activesupport (= 5.1.5)
|
||||
activerecord (5.1.5)
|
||||
activemodel (= 5.1.5)
|
||||
activesupport (= 5.1.5)
|
||||
arel (~> 8.0)
|
||||
activesupport (5.1.5)
|
||||
activemodel (7.0.1)
|
||||
activesupport (= 7.0.1)
|
||||
activerecord (7.0.1)
|
||||
activemodel (= 7.0.1)
|
||||
activesupport (= 7.0.1)
|
||||
activesupport (7.0.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
arel (8.0.0)
|
||||
concurrent-ruby (1.0.5)
|
||||
daemons (1.2.6)
|
||||
et-orbi (1.0.9)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
concurrent-ruby (1.1.9)
|
||||
daemons (1.4.1)
|
||||
et-orbi (1.2.6)
|
||||
tzinfo
|
||||
i18n (0.9.5)
|
||||
fugit (1.5.2)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.4)
|
||||
i18n (1.8.11)
|
||||
concurrent-ruby (~> 1.0)
|
||||
mail (2.7.0)
|
||||
mail (2.7.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
mini_mime (1.0.0)
|
||||
minitest (5.11.3)
|
||||
mysql2 (0.4.10)
|
||||
rufus-scheduler (3.4.2)
|
||||
et-orbi (~> 1.0)
|
||||
serialport (1.3.1)
|
||||
mini_mime (1.1.2)
|
||||
minitest (5.15.0)
|
||||
mysql2 (0.5.3)
|
||||
raabro (1.4.0)
|
||||
rufus-scheduler (3.8.0)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
serialport (1.3.2)
|
||||
state_pattern (2.0.2)
|
||||
thread_safe (0.3.6)
|
||||
tzinfo (1.2.5)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@@ -45,4 +46,4 @@ DEPENDENCIES
|
||||
state_pattern
|
||||
|
||||
BUNDLED WITH
|
||||
1.16.1
|
||||
2.1.4
|
||||
|
||||
35
app/models/entsoe.rb
Normal file
35
app/models/entsoe.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
#
|
||||
# Obtain energy prices from Entsoe (transparancy platform)
|
||||
# See https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html
|
||||
#
|
||||
|
||||
require 'open-uri'
|
||||
require 'nokogiri'
|
||||
|
||||
class Entsoe
|
||||
|
||||
URL = 'https://transparency.entsoe.eu'
|
||||
|
||||
def initialize(api_key)
|
||||
@api_key = api_key
|
||||
end
|
||||
|
||||
def day_ahead_prices(date)
|
||||
start_date = date.beginning_of_day
|
||||
end_date = date.end_of_day
|
||||
|
||||
# A44 - Document type => Price Document
|
||||
# A01 - Process type => Day Ahead
|
||||
# NL = '10YNL----------L'
|
||||
|
||||
domain = '10YNL----------L'
|
||||
url = URL + "/api?securityToken=%s&documentType=A44&processType=A01&in_Domain=%s&out_Domain=%s&periodStart=%s&periodEnd=%s" % [@api_key, domain, domain, start_date.iso8601, end_date.iso8601]
|
||||
p url
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_request(url)
|
||||
doc = Nokogiri::XML(URI.open(url))
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,5 @@
|
||||
require 'open-uri'
|
||||
|
||||
class Reading < ActiveRecord::Base
|
||||
|
||||
def eql_reading?(reading)
|
||||
@@ -49,6 +51,8 @@ class Reading < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Class methods
|
||||
#
|
||||
@@ -62,6 +66,258 @@ class Reading < ActiveRecord::Base
|
||||
Reading.where("created_at > :begin AND created_at < :end", { :begin => date.to_date.beginning_of_day, :end => date.to_date.end_of_day})
|
||||
end
|
||||
|
||||
EASY_ENERGY_TARIFFS = {}
|
||||
|
||||
|
||||
# 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(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 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
|
||||
|
||||
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
|
||||
|
||||
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 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
|
||||
|
||||
|
||||
UNKNOWN_READING = { :total_kwh_consumed_high => nil, :total_kwh_consumed_low => nil, :total_kwh_produced_high => nil, :total_kwh_produced_low => nil, :total_m3_gas_consumed => nil}
|
||||
|
||||
def format_cost(cost)
|
||||
cost ? "EUR %0.03f" % cost : "EUR ?"
|
||||
end
|
||||
|
||||
@battery_kwh = 0.0
|
||||
BATTERY_MAX_KWH = 10.0
|
||||
|
||||
def charge_battery(kwh)
|
||||
return 0.0 if kwh.nil?
|
||||
if @battery_kwh + kwh <= BATTERY_MAX_KWH
|
||||
@battery_kwh += kwh.to_f
|
||||
p "Battery is now at %s kwh" % @battery_kwh
|
||||
return kwh
|
||||
else
|
||||
old_kwh = @battery_kwh
|
||||
@battery_kwh = BATTERY_MAX_KWH
|
||||
p "Battery is now at %s kwh" % @battery_kwh
|
||||
return (BATTERY_MAX_KWH-old_kwh)
|
||||
end
|
||||
end
|
||||
|
||||
def discharge_battery(kwh)
|
||||
return 0.0 if kwh.nil?
|
||||
if @battery_kwh > kwh.to_f
|
||||
@battery_kwh -= kwh.to_f
|
||||
p "Battery is now at %s kwh" % @battery_kwh
|
||||
return kwh
|
||||
else
|
||||
old_kwh = @battery_kwh
|
||||
@battery_kwh = 0.0
|
||||
p "Battery is now at %s kwh" % @battery_kwh
|
||||
return old_kwh
|
||||
end
|
||||
end
|
||||
|
||||
MAX_CHARGE_KWH = 7.0
|
||||
|
||||
def hours(date, year_shift=0)
|
||||
hour_start = date.beginning_of_day
|
||||
day_end = hour_start.advance(days: 1)
|
||||
result = []
|
||||
low_hour, high_hour = easy_energy_high_low(date)
|
||||
low_usage_rate, low_return_rate = easy_energy_rate(low_hour)
|
||||
|
||||
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
|
||||
easy_cost = easy_energy_cost(formatted_hour, usage_kwh, return_kwh) # without battery use
|
||||
|
||||
if !usage_kwh.nil?
|
||||
# charge battery with return_kwh
|
||||
return_kwh -= charge_battery(return_kwh)
|
||||
|
||||
if low_hour.eql?(formatted_hour)
|
||||
# charge battery during lowest hour
|
||||
usage_kwh += charge_battery(MAX_CHARGE_KWH)
|
||||
else
|
||||
easy_usage_rate, easy_return_rate = easy_energy_rate(formatted_hour)
|
||||
# if rate > charge_rate + 0.05 || more than 5.0 kwh then discharge_battery
|
||||
if @battery_kwh > 5.0 || (easy_usage_rate.to_f > (low_usage_rate.to_f + 0.05))
|
||||
usage_kwh -= discharge_battery(usage_kwh)
|
||||
end
|
||||
end
|
||||
end
|
||||
easy_cost_with_battery = easy_energy_cost(formatted_hour, usage_kwh, return_kwh) # with battery use
|
||||
|
||||
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_kwh,
|
||||
easy_usage_rate,
|
||||
easy_return_rate,
|
||||
easy_cost_with_battery,
|
||||
easy_cost,
|
||||
oxxio_rate,
|
||||
oxxio_cost]
|
||||
|
||||
p "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" % (one_hour[0..7] + one_hour[8..13].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
|
||||
easy_cost_with_battery = hours.map{|e| e[10]}.compact.sum
|
||||
easy_cost = hours.map{|e| e[11]}.compact.sum
|
||||
oxxio_cost = hours.map{|e| e[13]}.compact.sum
|
||||
|
||||
return usage_kwh, return_kwh, usage_kwh_with_battery, return_kwh_with_battery, format_cost(easy_cost_with_battery), format_cost(easy_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
|
||||
|
||||
|
||||
####
|
||||
|
||||
def diff_on(date)
|
||||
readings_on = day(date)
|
||||
first = readings_on.first
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require "rubygems"
|
||||
require "bundler/setup"
|
||||
require "active_record"
|
||||
require "open-uri"
|
||||
|
||||
project_root = File.dirname(File.absolute_path(__FILE__))
|
||||
Dir.glob(project_root + "/app/models/*.rb").each{|f| require f}
|
||||
|
||||
5
db/migrate/002_add_created_at_index_to_readings.rb
Normal file
5
db/migrate/002_add_created_at_index_to_readings.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddCreatedAtIndexToReadings < ActiveRecord::Migration[4.2]
|
||||
def change
|
||||
add_index :readings, :created_at
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user