From 5c7b765513662ef7df4f4a4d48a881695d6c4efa Mon Sep 17 00:00:00 2001 From: Georg Ledermann Date: Wed, 24 Jul 2024 16:31:55 +0200 Subject: [PATCH] Wallbox or heat pump may be missing, but not both --- README.md | 4 +-- lib/config.rb | 39 ++++++++++++++++++++++++++-- lib/flux/extractor.rb | 2 +- lib/flux/first_sensor.rb | 2 +- lib/processor.rb | 27 ++++++++++++++----- spec/lib/config_spec.rb | 56 +++++++++++++++++++++++++++++++++++++++- 6 files changed, 116 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 29737ec..4585ae4 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,7 @@ This enables SOLECTRUS to accurately calculate the electricity usage and costs f - InfluxDB 2 database with a bucket filled with values for: - Grid import power - House power - - Heat pump power (optional) - - Wallbox power (optional) + - Heatpump power and/or Wallbox power - Linux machine with Docker installed ## Getting started @@ -32,7 +31,6 @@ This enables SOLECTRUS to accurately calculate the electricity usage and costs f The Docker image supports multiple platforms: `linux/amd64`, `linux/arm64` - To force a data rebuild, you can delete the measurement from the InfluxDB database: ```bash diff --git a/lib/config.rb b/lib/config.rb index 77137e6..3487787 100644 --- a/lib/config.rb +++ b/lib/config.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext' require 'null_logger' -class Config +class Config # rubocop:disable Metrics/ClassLength attr_accessor :influx_schema, :influx_host, :influx_port, @@ -32,6 +32,7 @@ def initialize(env, logger: NullLogger.new) @time_zone = env.fetch('TZ', 'Europe/Berlin') init_sensors(env) + validate_sensors! end def influx_url @@ -50,6 +51,20 @@ def field(sensor_name) @field[sensor_name] ||= splitted_sensor_name(sensor_name)&.last end + def exists?(sensor_name) + case sensor_name + when *SENSOR_NAMES + measurement(sensor_name).present? && field(sensor_name).present? + else + raise ArgumentError, + "Unknown or invalid sensor name: #{sensor_name.inspect}" + end + end + + def sensor_names + @sensor_names ||= SENSOR_NAMES.filter { |sensor_name| exists?(sensor_name) } + end + private def validate_url!(url) @@ -60,7 +75,8 @@ def init_sensors(env) logger.info 'Sensor initialization started' SENSOR_NAMES.each do |sensor_name| var_sensor = var_for(sensor_name) - value = env.fetch(var_sensor) + value = env.fetch(var_sensor, nil) + next unless value validate!(sensor_name, value) define_sensor(sensor_name, value) @@ -72,6 +88,23 @@ def init_sensors(env) logger.info 'Sensor initialization completed' end + def validate_sensors! + unless exists?(:wallbox_power) || exists?(:heatpump_power) + raise Error, + 'At least one of INFLUX_SENSOR_WALLBOX_POWER or INFLUX_SENSOR_HEATPUMP_POWER must be set.' + end + + unless exists?(:grid_import_power) + raise Error, 'INFLUX_SENSOR_GRID_IMPORT_POWER must be set.' + end + + unless exists?(:house_power) + raise Error, 'INFLUX_SENSOR_HOUSE_POWER must be set.' + end + + true + end + class Error < RuntimeError end @@ -130,6 +163,8 @@ def validate!(sensor_name, value) end def splitted_sensor_name(sensor_name) + return unless respond_to?(sensor_name.downcase) + public_send(sensor_name.downcase)&.split(':') end end diff --git a/lib/flux/extractor.rb b/lib/flux/extractor.rb index 0b8f1c3..7d77e43 100644 --- a/lib/flux/extractor.rb +++ b/lib/flux/extractor.rb @@ -8,7 +8,7 @@ def records(day) query_string = <<~FLUX #{from_bucket} |> #{day_range(day)} - |> #{filter(selected_sensors: Config::SENSOR_NAMES)} + |> #{filter(selected_sensors: config.sensor_names)} |> aggregateWindow(every: 1m, fn: mean) |> fill(usePrevious: true) FLUX diff --git a/lib/flux/first_sensor.rb b/lib/flux/first_sensor.rb index e3baf96..6f83bcb 100644 --- a/lib/flux/first_sensor.rb +++ b/lib/flux/first_sensor.rb @@ -6,7 +6,7 @@ def time query_string = <<~FLUX #{from_bucket} |> #{range(start: Time.at(0))} - |> #{filter(selected_sensors: Config::SENSOR_NAMES)} + |> #{filter(selected_sensors: config.sensor_names)} |> first() |> keep(columns: ["_time"]) |> min(column: "_time") diff --git a/lib/processor.rb b/lib/processor.rb index 55d385d..d0b46d7 100644 --- a/lib/processor.rb +++ b/lib/processor.rb @@ -5,9 +5,12 @@ class Processor def initialize(day_records:, config:) @day_records = day_records @config = config + + @wallbox_present = config.exists?(:wallbox_power) + @heatpump_present = config.exists?(:heatpump_power) end - attr_reader :day_records, :config + attr_reader :day_records, :config, :wallbox_present, :heatpump_present def call group_by_hour( @@ -25,8 +28,14 @@ def point(record) ) result.add_field('house_power_grid', record[:house_power_grid]) - result.add_field('wallbox_power_grid', record[:wallbox_power_grid]) - result.add_field('heatpump_power_grid', record[:heatpump_power_grid]) + + if wallbox_present + result.add_field('wallbox_power_grid', record[:wallbox_power_grid]) + end + + if heatpump_present + result.add_field('heatpump_power_grid', record[:heatpump_power_grid]) + end result end @@ -38,9 +47,11 @@ def group_by_hour(splitted) { time: items.first[:time].beginning_of_hour, house_power_grid: sum(items, :house_power_grid), - wallbox_power_grid: sum(items, :wallbox_power_grid), - heatpump_power_grid: sum(items, :heatpump_power_grid), - } + wallbox_power_grid: + wallbox_present ? sum(items, :wallbox_power_grid) : nil, + heatpump_power_grid: + heatpump_present ? sum(items, :heatpump_power_grid) : nil, + }.compact end end @@ -59,10 +70,14 @@ def house_power(record) end def wallbox_power(record) + return 0 unless config.exists?(:wallbox_power) + power_value(record, :wallbox_power) end def heatpump_power(record) + return 0 unless config.exists?(:heatpump_power) + power_value(record, :heatpump_power) end diff --git a/spec/lib/config_spec.rb b/spec/lib/config_spec.rb index 83ab4e1..695b3bc 100644 --- a/spec/lib/config_spec.rb +++ b/spec/lib/config_spec.rb @@ -30,6 +30,22 @@ end end + describe 'valid options (no wallbox)' do + let(:env) { valid_env.except('INFLUX_SENSOR_WALLBOX_POWER') } + + it 'initializes successfully' do + expect(config).to be_a(described_class) + end + end + + describe 'valid options (no heatpump)' do + let(:env) { valid_env.except('INFLUX_SENSOR_HEATPUMP_POWER') } + + it 'initializes successfully' do + expect(config).to be_a(described_class) + end + end + describe 'Influx methods' do let(:env) { valid_env } @@ -50,7 +66,45 @@ let(:env) { {} } it 'raises an exception' do - expect { described_class.new(env) }.to raise_error(Exception) + expect { described_class.new(env) }.to raise_error(KeyError) + end + end + + context 'when no house_power' do + let(:env) { valid_env.except('INFLUX_SENSOR_HOUSE_POWER') } + + it 'raises an exception' do + expect { described_class.new(env) }.to raise_error( + Config::Error, + /must be set/, + ) + end + end + + context 'when no grid_import_power' do + let(:env) { valid_env.except('INFLUX_SENSOR_GRID_IMPORT_POWER') } + + it 'raises an exception' do + expect { described_class.new(env) }.to raise_error( + Config::Error, + /must be set/, + ) + end + end + + context 'when no heatpump AND no wallbox' do + let(:env) do + valid_env.except( + 'INFLUX_SENSOR_HEATPUMP_POWER', + 'INFLUX_SENSOR_WALLBOX_POWER', + ) + end + + it 'raises an exception' do + expect { described_class.new(env) }.to raise_error( + Config::Error, + /At least one of/, + ) end end end