From ba1c53436fd30aba1f676791cd140590e12e486a Mon Sep 17 00:00:00 2001 From: Georg Ledermann Date: Sun, 24 Nov 2024 18:22:27 +0100 Subject: [PATCH] Add optional "essential" mode --- .env.example | 5 +++ lib/config.rb | 8 ++++ lib/shelly_pull.rb | 36 +++++++++++++++- lib/solectrus_record.rb | 4 ++ spec/lib/config_spec.rb | 11 +++++ spec/lib/shelly_pull_spec.rb | 72 +++++++++++++++++++++++++++++++ spec/lib/solectrus_record_spec.rb | 53 +++++++++++++++++++++++ 7 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 spec/lib/solectrus_record_spec.rb diff --git a/.env.example b/.env.example index e4a49a6..ee74a98 100644 --- a/.env.example +++ b/.env.example @@ -17,3 +17,8 @@ INFLUX_ORG=solectrus # Customize InfluxDB storage INFLUX_BUCKET=my-solectrus-bucket INFLUX_MEASUREMENT=shelly + +# Optional: Essential mode to reduce data sent to InfluxDB +# When enabled, it minimizes the data sent to InfluxDB +# by skipping consecutive records with zero power values. +# INFLUX_MODE=essential diff --git a/lib/config.rb b/lib/config.rb index c56812d..3f71f53 100644 --- a/lib/config.rb +++ b/lib/config.rb @@ -14,6 +14,7 @@ influx_org influx_bucket influx_measurement + influx_mode ].freeze DEFAULTS = { @@ -22,6 +23,7 @@ influx_schema: :http, influx_port: 8086, influx_measurement: 'Consumer', + influx_mode: :default, }.freeze Config = @@ -117,6 +119,7 @@ def validate_influx_settings! end validate_url!(influx_url) + validate_mode!(influx_mode) end def validate_url!(url) @@ -125,6 +128,10 @@ def validate_url!(url) (uri.is_a?(URI::HTTP) && uri.host.present?) || throw("URL is invalid: #{url}") end + def validate_mode!(mode) + %i[default essential].include?(mode) || throw("INFLUX_MODE is invalid: #{mode}") + end + def self.from_env(options = {}) new( { @@ -138,6 +145,7 @@ def self.from_env(options = {}) influx_org: ENV.fetch('INFLUX_ORG'), influx_bucket: ENV.fetch('INFLUX_BUCKET', nil), influx_measurement: ENV.fetch('INFLUX_MEASUREMENT', nil), + influx_mode: ENV.fetch('INFLUX_MODE', nil)&.to_sym, }.merge(options), ) end diff --git a/lib/shelly_pull.rb b/lib/shelly_pull.rb index 16aff16..e565aa6 100644 --- a/lib/shelly_pull.rb +++ b/lib/shelly_pull.rb @@ -3,14 +3,48 @@ def initialize(config:, queue:) @queue = queue @config = config @count = 0 + @last_record = nil end attr_reader :config, :queue, :count def next record = config.adapter.solectrus_record(@count += 1) - queue << record + + queue << record if should_queue?(record) + queue << @last_record if should_queue_last_record?(record) + + # Remember the last record for the next iteration + @last_record = record record end + + private + + def should_queue?(record) + case config.influx_mode + when :default + # Always queue this record + true + + when :essential + # Only queue this record if the power is non-zero or if it just changed to zero + result = record.power? || @last_record.nil? || @last_record.power? + config.logger.info "Ignoring record ##{record.id} (zero power)" unless result + + result + end + end + + def should_queue_last_record?(record) + case config.influx_mode + when :default + false + + when :essential + # To ensure that a curve always starts at zero, we queue the last record (if it changes from zero) + record.power? && @last_record && !@last_record.power? + end + end end diff --git a/lib/solectrus_record.rb b/lib/solectrus_record.rb index da385f2..c398656 100644 --- a/lib/solectrus_record.rb +++ b/lib/solectrus_record.rb @@ -23,4 +23,8 @@ def to_hash @payload[method] end end + + def power? + !power.round.zero? + end end diff --git a/spec/lib/config_spec.rb b/spec/lib/config_spec.rb index c23bf88..b172fa1 100644 --- a/spec/lib/config_spec.rb +++ b/spec/lib/config_spec.rb @@ -7,6 +7,7 @@ influx_token: 'this.is.just.an.example', influx_org: 'solectrus', influx_bucket: 'Consumer', + influx_mode: :essential, } end @@ -51,6 +52,12 @@ end.to raise_error(Exception, /INFLUX_TOKEN is missing/) end + it 'raises an error for invalid INFLUX_MODE' do + expect do + described_class.new(valid_options.merge(influx_mode: 'foo')) + end.to raise_error(Exception, /MODE is invalid/) + end + it 'initializes with valid options' do expect { described_class.new(valid_options) }.not_to raise_error end @@ -138,5 +145,9 @@ it 'returns correct influx_measurement' do expect(config.influx_measurement).to eq('Consumer') end + + it 'returns correct influx_mode' do + expect(config.influx_mode).to eq(:essential) + end end end diff --git a/spec/lib/shelly_pull_spec.rb b/spec/lib/shelly_pull_spec.rb index 5596a11..ad2ecd7 100644 --- a/spec/lib/shelly_pull_spec.rb +++ b/spec/lib/shelly_pull_spec.rb @@ -31,5 +31,77 @@ expect(queue.length).to eq(0) end end + + context 'when essential mode' do + let(:config) { Config.from_env(influx_mode: :essential) } + + it 'queues record if power is non-zero' do + expect do + record1 = SolectrusRecord.new(id: 1, time: 1, payload: { power: 1 }) + allow(config.adapter).to receive(:solectrus_record).and_return(record1) + shelly_pull.next + end.to change(queue, :length).by(1) # Because it's non-zero + + expect do + record2 = SolectrusRecord.new(id: 2, time: 2, payload: { power: 2 }) + allow(config.adapter).to receive(:solectrus_record).and_return(record2) + shelly_pull.next + end.to change(queue, :length).by(1) # Because it's non-zero + end + + it 'queues record if power just changed to zero' do + expect do + record1 = SolectrusRecord.new(id: 3, time: 3, payload: { power: 1 }) + allow(config.adapter).to receive(:solectrus_record).and_return(record1) + shelly_pull.next + end.to change(queue, :length).by(1) # Because it's non-zero + + expect do + record2 = SolectrusRecord.new(id: 4, time: 4, payload: { power: 0 }) + allow(config.adapter).to receive(:solectrus_record).and_return(record2) + shelly_pull.next + end.to change(queue, :length).by(1) # Because power just changed to zero + end + + it 'does not queue record if power is zero' do + expect do + record1 = SolectrusRecord.new(id: 5, time: 5, payload: { power: 0 }) + allow(config.adapter).to receive(:solectrus_record).and_return(record1) + shelly_pull.next + end.to change(queue, :length) # Because it's the first record + + expect do + record2 = SolectrusRecord.new(id: 6, time: 6, payload: { power: 0 }) + allow(config.adapter).to receive(:solectrus_record).and_return(record2) + shelly_pull.next + end.not_to change(queue, :length) # Because power is still zero + end + + it 'does queue last_record before non-zero' do # rubocop:disable RSpec/MultipleExpectations + expect do + record1 = SolectrusRecord.new(id: 7, time: 7, payload: { power: 0 }) + allow(config.adapter).to receive(:solectrus_record).and_return(record1) + shelly_pull.next + end.to change(queue, :length).by(1) # Because it's the first record + + expect do + record2 = SolectrusRecord.new(id: 8, time: 8, payload: { power: 0 }) + allow(config.adapter).to receive(:solectrus_record).and_return(record2) + shelly_pull.next + end.not_to change(queue, :length) # Because power is zero + + expect do + record3 = SolectrusRecord.new(id: 9, time: 9, payload: { power: 0 }) + allow(config.adapter).to receive(:solectrus_record).and_return(record3) + shelly_pull.next + end.not_to(change(queue, :length)) # Because power is still zero + + expect do + record4 = SolectrusRecord.new(id: 10, time: 9, payload: { power: 1 }) + allow(config.adapter).to receive(:solectrus_record).and_return(record4) + shelly_pull.next + end.to change(queue, :length).by(2) # Because power is non-zero and last_record power was zero + end + end end end diff --git a/spec/lib/solectrus_record_spec.rb b/spec/lib/solectrus_record_spec.rb new file mode 100644 index 0000000..038f3c8 --- /dev/null +++ b/spec/lib/solectrus_record_spec.rb @@ -0,0 +1,53 @@ +require 'solectrus_record' + +describe SolectrusRecord do + subject(:record) { described_class.new(id: 1, time: Time.now, payload:) } + + let(:payload) do + { + temp: 25.5, + power: 0.0, + power_a: 10.2, + power_b: 20.5, + power_c: 30.3, + response_duration: 20.5, + } + end + + describe '#initialize' do + it 'assigns id and time' do + expect(record.id).to eq(1) + expect(record.time).to be_a(Time) + end + end + + describe '#to_hash' do + it 'returns the payload hash' do + expect(record.to_hash).to eq(payload) + end + end + + describe '#power?' do + context 'when power is zero' do + it 'returns false' do + expect(record.power?).to be(false) + end + end + + context 'when power is non-zero' do + let(:payload) { super().merge(power: 42) } + + it 'returns true' do + expect(record.power?).to be(true) + end + end + end + + %i[temp power power_a power_b power_c response_duration].each do |method| + describe "##{method}" do + it "returns the value of #{method} from the payload" do + expect(record.send(method)).to eq(payload[method]) + end + end + end +end