diff --git a/.ruby-version b/.ruby-version index b503b50..fa376ed 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-2.4.3 +ruby-2.7 diff --git a/Dockerfile b/Dockerfile index fd327f5..abd6ca0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 && \ diff --git a/Gemfile.lock b/Gemfile.lock index d4ad127..c549dcc 100644 --- a/Gemfile.lock +++ b/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 diff --git a/app/models/entsoe.rb b/app/models/entsoe.rb new file mode 100644 index 0000000..6bad49b --- /dev/null +++ b/app/models/entsoe.rb @@ -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 diff --git a/app/models/reading.rb b/app/models/reading.rb index b07260e..5b08fa0 100644 --- a/app/models/reading.rb +++ b/app/models/reading.rb @@ -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 diff --git a/ar-no-rails.rb b/ar-no-rails.rb index 3ff54ec..60a853c 100644 --- a/ar-no-rails.rb +++ b/ar-no-rails.rb @@ -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} diff --git a/db/migrate/002_add_created_at_index_to_readings.rb b/db/migrate/002_add_created_at_index_to_readings.rb new file mode 100644 index 0000000..f527a6f --- /dev/null +++ b/db/migrate/002_add_created_at_index_to_readings.rb @@ -0,0 +1,5 @@ +class AddCreatedAtIndexToReadings < ActiveRecord::Migration[4.2] + def change + add_index :readings, :created_at + end +end