diff --git a/Gemfile b/Gemfile index fd04203..353b524 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,7 @@ gem "tailwindcss-rails" gem "turbo-rails" ## LIBRARIES +gem "active_median" gem "bindata" gem "bootsnap", require: false gem "concurrent-ruby" diff --git a/Gemfile.lock b/Gemfile.lock index 8a333ec..b9fe20c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -49,6 +49,8 @@ GEM erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) + active_median (0.4.0) + activesupport (>= 6.1) activejob (7.1.1) activesupport (= 7.1.1) globalid (>= 0.3.6) @@ -327,6 +329,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + active_median amazing_print better_errors bindata diff --git a/app/lib/loops/archive.rb b/app/lib/loops/archive.rb new file mode 100644 index 0000000..9e1465c --- /dev/null +++ b/app/lib/loops/archive.rb @@ -0,0 +1,15 @@ +module Loops + class Archive + def self.run(store) + new(store).run + end + + def initialize(store) + @store = store + end + + def run + ::Archive.create!(@store.get.merge(datetime: Time.current)) + end + end +end diff --git a/app/lib/loops/main.rb b/app/lib/loops/main.rb new file mode 100644 index 0000000..33684e1 --- /dev/null +++ b/app/lib/loops/main.rb @@ -0,0 +1,15 @@ +module Loops + class Main + def self.run(store) + new(store).run + end + + def initialize(store) + @store = store + end + + def run + @store.set(Inverter.read) + end + end +end diff --git a/app/lib/metric_store.rb b/app/lib/metric_store.rb new file mode 100644 index 0000000..75ec9e1 --- /dev/null +++ b/app/lib/metric_store.rb @@ -0,0 +1,46 @@ +class MetricStore + def initialize + @mutex = Mutex.new + reset + end + + def set(metrics) + @mutex.synchronize do + @metrics.each_key do |key| + @metrics[key] << metrics[key] if metrics.key?(key) + end + end + end + + def get + @mutex.synchronize do + @metrics.to_h { |key, values| [key, send("reduce_#{key}", values)] }.tap { reset } + end + end + + private + + def reset + @metrics = { solar_power: [], solar_energy: [], grid_power: [], grid_energy_import: [], grid_energy_export: [] } + end + + def reduce_solar_power(values) + values.median || 0 + end + + def reduce_solar_energy(values) + values.max || 0.0 + end + + def reduce_grid_power(values) + values.median || 0 + end + + def reduce_grid_energy_import(values) + values.max || 0.0 + end + + def reduce_grid_energy_export(values) + values.max || 0.0 + end +end diff --git a/app/services/loop.rb b/app/services/loop.rb deleted file mode 100644 index 081bd83..0000000 --- a/app/services/loop.rb +++ /dev/null @@ -1,31 +0,0 @@ -class Loop - def self.run - new.run - end - - def run - perform_archive if condition_for_archive - end - - private - - def archive_interval - Rails.configuration.x.intervals.archive - end - - def last_archive - @last_archive ||= Archive.order(datetime: :desc).first - end - - def condition_for_archive - last_archive.nil? || last_archive.datetime < archive_interval.seconds.ago - end - - def perform_archive - Archive.create!(read_inverter_data.merge(datetime: Time.current)).persisted? - end - - def read_inverter_data - Inverter.read - end -end diff --git a/config/scheduler.rb b/config/scheduler.rb index 809f635..68b0e74 100644 --- a/config/scheduler.rb +++ b/config/scheduler.rb @@ -1,8 +1,14 @@ scheduler = Rufus::Scheduler.new loop_interval = Rails.configuration.x.intervals.loop +archive_interval = Rails.configuration.x.intervals.archive +store = MetricStore.new -scheduler.every loop_interval, name: "solaris.loop" do - Loop.run +scheduler.every loop_interval, name: "solaris.loop.main" do + Loops::Main.run(store) +end + +scheduler.every archive_interval, name: "solaris.loop.archive" do + Loops::Archive.run(store) end if Rails.configuration.x.esios.api_key.present? diff --git a/test/lib/loops/archive_test.rb b/test/lib/loops/archive_test.rb new file mode 100644 index 0000000..3550111 --- /dev/null +++ b/test/lib/loops/archive_test.rb @@ -0,0 +1,25 @@ +require "test_helper" + +module Loops + class ArchiveTest < ActiveSupport::TestCase + test "create archive with stored data" do + store = MetricStore.new + + store.set(solar_power: 1, solar_energy: 4, grid_power: 7, grid_energy_import: 10, grid_energy_export: 13) + store.set(solar_power: 2, solar_energy: 5, grid_power: 8, grid_energy_import: 11, grid_energy_export: 14) + store.set(solar_power: 3, solar_energy: 6, grid_power: 9, grid_energy_import: 12, grid_energy_export: 15) + + assert_difference("::Archive.count", 1) do + Loops::Archive.run(store) + end + + archive = ::Archive.last + + assert_equal 2, archive.solar_power + assert_in_delta(6.0, archive.solar_energy) + assert_equal 8, archive.grid_power + assert_in_delta(12.0, archive.grid_energy_import) + assert_in_delta(15.0, archive.grid_energy_export) + end + end +end diff --git a/test/lib/loops/main_test.rb b/test/lib/loops/main_test.rb new file mode 100644 index 0000000..cd39d9e --- /dev/null +++ b/test/lib/loops/main_test.rb @@ -0,0 +1,15 @@ +require "test_helper" + +module Loops + class MainTest < ActiveSupport::TestCase + test "store inverter data" do + store = Minitest::Mock.new + store.expect(:set, nil, [{ solar_power: 123 }]) + + Inverter.stub(:read, { solar_power: 123 }) do + Loops::Main.run(store) + store.verify + end + end + end +end diff --git a/test/lib/metric_store_test.rb b/test/lib/metric_store_test.rb new file mode 100644 index 0000000..545a20b --- /dev/null +++ b/test/lib/metric_store_test.rb @@ -0,0 +1,40 @@ +require "test_helper" + +class MetricStoreTest < ActiveSupport::TestCase + test "get empty" do + store = MetricStore.new + + assert_equal( + { solar_power: 0, solar_energy: 0.0, grid_power: 0, grid_energy_import: 0.0, grid_energy_export: 0.0 }, + store.get + ) + end + + test "get with values" do + store = MetricStore.new + + store.set(solar_power: 1, solar_energy: 4, grid_power: 7, grid_energy_import: 10, grid_energy_export: 13) + store.set(solar_power: 2, solar_energy: 5, grid_power: 8, grid_energy_import: 11, grid_energy_export: 14) + store.set(solar_power: 3, solar_energy: 6, grid_power: 9, grid_energy_import: 12, grid_energy_export: 15) + + assert_equal( + { solar_power: 2, solar_energy: 6.0, grid_power: 8, grid_energy_import: 12.0, grid_energy_export: 15.0 }, + store.get + ) + end + + test "reset after get" do + store = MetricStore.new + + store.set(solar_power: 1, solar_energy: 4, grid_power: 7, grid_energy_import: 10, grid_energy_export: 13) + store.set(solar_power: 2, solar_energy: 5, grid_power: 8, grid_energy_import: 11, grid_energy_export: 14) + store.set(solar_power: 3, solar_energy: 6, grid_power: 9, grid_energy_import: 12, grid_energy_export: 15) + + store.get + + assert_equal( + { solar_power: 0, solar_energy: 0.0, grid_power: 0, grid_energy_import: 0.0, grid_energy_export: 0.0 }, + store.get + ) + end +end diff --git a/test/services/loop_test.rb b/test/services/loop_test.rb deleted file mode 100644 index 73ce4f9..0000000 --- a/test/services/loop_test.rb +++ /dev/null @@ -1,52 +0,0 @@ -require "test_helper" - -class LoopTest < ActiveSupport::TestCase - setup do - Archive.delete_all - travel_to Time.zone.parse("2024-01-01 00:00:00") - end - - test "create archive if not exists any archive" do - Inverter.stub(:read, inverter_response) do - Rails.configuration.x.intervals.with(archive: 5) do - assert_difference("Archive.count", 1) do - Loop.run - end - end - end - end - - test "create archive if last archive is older than archive interval" do - Archive.create(inverter_response.merge(datetime: 10.seconds.ago)) - - Inverter.stub(:read, inverter_response) do - Rails.configuration.x.intervals.with(archive: 5) do - assert_difference("Archive.count", 1) do - Loop.run - end - end - end - end - - test "not create archive if last archive is newer than archive interval" do - Archive.create(inverter_response.merge(datetime: 4.seconds.ago)) - - Inverter.stub(:read, inverter_response) do - Rails.configuration.x.intervals.with(archive: 5) do - assert_no_difference("Archive.count") do - Loop.run - end - end - end - end - - def inverter_response - { - solar_power: 1, - solar_energy: 2, - grid_power: 3, - grid_energy_export: 4, - grid_energy_import: 5 - } - end -end