Files
smartmeter/app/models/reading.rb
Aart van Halteren 6db550c11f use URI::open
2022-01-09 19:32:45 +01:00

333 lines
13 KiB
Ruby

require 'open-uri'
class Reading < ActiveRecord::Base
def eql_reading?(reading)
self.total_kwh_consumed_high == reading.total_kwh_consumed_high &&
self.total_kwh_consumed_low == reading.total_kwh_consumed_low &&
self.total_kwh_produced_high == reading.total_kwh_produced_high &&
self.total_kwh_produced_low == reading.total_kwh_produced_low &&
self.current_kw_consumed == reading.current_kw_consumed &&
self.current_kw_produced == reading.current_kw_produced &&
self.total_m3_gas_consumed == reading.total_m3_gas_consumed &&
self.high_tarif == reading.high_tarif
end
# reduce precision to 1 digit behind comma
def total_kwh_consumed_high=(kwh)
write_attribute(:total_kwh_consumed_high,kwh.round(1))
end
# reduce precision to 1 digit behind comma
def total_kwh_consumed_low=(kwh)
write_attribute(:total_kwh_consumed_low,kwh.round(1))
end
# reduce precision to 1 digit behind comma
def total_kwh_produced_high=(kwh)
write_attribute(:total_kwh_produced_high,kwh.round(1))
end
# reduce precision to 1 digit behind comma
def total_kwh_produced_low=(kwh)
write_attribute(:total_kwh_produced_low,kwh.round(1))
end
# calculate difference with another reading
# return a hash with differences (self - reading)
def diff(reading)
if reading
{ :total_kwh_consumed_high => (self.total_kwh_consumed_high - reading.total_kwh_consumed_high).round(1),
:total_kwh_consumed_low => (self.total_kwh_consumed_low - reading.total_kwh_consumed_low).round(1),
:total_kwh_produced_high => (self.total_kwh_produced_high - reading.total_kwh_produced_high).round(1),
:total_kwh_produced_low => (self.total_kwh_produced_low - reading.total_kwh_produced_low).round(1),
:total_m3_gas_consumed => (self.total_m3_gas_consumed - reading.total_m3_gas_consumed).round(3) }
else
{ :total_kwh_consumed_high => self.total_kwh_consumed_high,
:total_kwh_consumed_low => self.total_kwh_consumed_low,
:total_kwh_produced_high => self.total_kwh_produced_high,
:total_kwh_produced_low => self.total_kwh_produced_low,
:total_m3_gas_consumed => self.total_m3_gas_consumed }
end
end
#
# Class methods
#
class << self
# return readings from beginning of 'from' until end of 'to'
def days(from, to)
Reading.where("created_at > :begin AND created_at < :end", { :begin => from.to_date.beginning_of_day, :end => to.to_date.end_of_day})
end
def day(date)
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(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 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
last = readings_on.last
if last
last.diff(first)
else
{ :total_kwh_consumed_high => 0, :total_kwh_consumed_low => 0, :total_kwh_produced_high => 0, :total_kwh_produced_low => 0, :total_m3_gas_consumed => 0 }
end
end
end
end