Skip to content

Commit

Permalink
Add more config options to ENV
Browse files Browse the repository at this point in the history
CHARGER_PRICE_MODE
CHARGER_PRICE_TIME_RANGE
CHARGER_FORECAST_THRESHOLD
  • Loading branch information
ledermann committed Dec 14, 2023
1 parent cb7c6a8 commit d13a3c9
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 19 deletions.
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ SENEC_SCHEMA=https
# Interval in seconds to check for charging
CHARGER_INTERVAL=3600

# Do you to charge when price level is VERY_CHEAP (strict) or do yo allow CHEAP (relaxed) prices as well?
CHARGER_PRICE_MODE=relaxed

# How many hours in advance should the charger check for cheap prices?
CHARGER_PRICE_TIME_RANGE=4

# At what PV yield (in kWh) in the upcoming 24h should charging from the grid be avoided?
CHARGER_FORECAST_THRESHOLD=20

# Tibber settings
TIBBER_TOKEN=my-tibber-token
TIBBER_INTERVAL=3600
Expand Down
35 changes: 30 additions & 5 deletions app/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
:senec_host,
:senec_schema,
:charger_interval,
:charger_price_mode,
:charger_price_time_range,
:charger_forecast_threshold,
:influx_schema,
:influx_host,
:influx_port,
Expand All @@ -19,6 +22,9 @@ def initialize(*options)
validate_url!(senec_url)
validate_url!(influx_url)
validate_interval!(charger_interval)
validate_price_mode!(charger_price_mode)
validate_price_time_range!(charger_price_time_range)
validate_forecast_threshold!(charger_forecast_threshold)
end

def influx_url
Expand All @@ -36,17 +42,31 @@ def senec_connection

private

def validate_interval!(charger_interval)
return if charger_interval.is_a?(Integer) && charger_interval.positive?
def validate_interval!(interval)
(interval.is_a?(Integer) && interval.positive?) ||
throw("Interval is invalid: #{interval}")
end

def validate_price_mode!(price_mode)
%i[strict relaxed].include?(price_mode) ||
throw("Price mode is invalid: #{price_mode}")
end

def validate_price_time_range!(price_time_range)
(price_time_range.is_a?(Integer) && price_time_range.positive?) ||
throw("Time range is invalid: #{price_time_range}")
end

throw "Interval is invalid: #{charger_interval}"
def validate_forecast_threshold!(forecast_threshold)
(forecast_threshold.is_a?(Integer) && forecast_threshold.positive?) ||
throw("Forecast threshold is invalid: #{forecast_threshold}")
end

def validate_url!(url)
uri = URI.parse(url)
return if uri.is_a?(URI::HTTP) && !uri.host.nil?

throw "URL is invalid: #{url}"
(uri.is_a?(URI::HTTP) && !uri.host.nil?) ||
throw("URL is invalid: #{url}")
end

def self.from_env(options = {})
Expand All @@ -55,6 +75,11 @@ def self.from_env(options = {})
senec_host: ENV.fetch('SENEC_HOST'),
senec_schema: ENV.fetch('SENEC_SCHEMA', 'https'),
charger_interval: ENV.fetch('CHARGER_INTERVAL', '3600').to_i,
charger_price_mode: ENV.fetch('CHARGER_PRICE_MODE', 'strict').to_sym,
charger_price_time_range:
ENV.fetch('CHARGER_PRICE_TIME_RANGE', '4').to_i,
charger_forecast_threshold:
ENV.fetch('CHARGER_FORECAST_THRESHOLD', '20').to_i,
influx_host: ENV.fetch('INFLUX_HOST'),
influx_schema: ENV.fetch('INFLUX_SCHEMA', 'http'),
influx_port: ENV.fetch('INFLUX_PORT', '8086'),
Expand Down
2 changes: 1 addition & 1 deletion app/forecast_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def initialize(config:)
attr_reader :config

def sunshine_ahead?
total_in_kwh > 20
total_in_kwh > config.charger_forecast_threshold
end

def total_in_kwh
Expand Down
19 changes: 15 additions & 4 deletions app/prices_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ def initialize(config:)

attr_reader :config

ACCEPTED_LEVELS = %w[CHEAP VERY_CHEAP].freeze

def cheap_grid_power?
levels.count { |level| ACCEPTED_LEVELS.include?(level) } >= 4
levels.count { |level| accepted_levels.include?(level) } >= time_range
end

def levels
Expand All @@ -21,6 +19,19 @@ def levels

private

def time_range
config.charger_price_time_range
end

def accepted_levels
case config.charger_price_mode
when :strict
%w[VERY_CHEAP]
when :relaxed
%w[CHEAP VERY_CHEAP]
end
end

def raw
# Is the last request less than 30min ago?
return @raw if @raw && @last_query_at && @last_query_at > Time.now - 1800
Expand All @@ -31,7 +42,7 @@ def raw

def query
"from(bucket: \"#{config.influx_bucket}\")
|> range(start: now(), stop: 4h)
|> range(start: now(), stop: #{time_range}h)
|> filter(fn: (r) => r[\"_measurement\"] == \"#{config.influx_measurement_prices}\")
|> filter(fn: (r) => r[\"_field\"] == \"#{field}\")
|> yield()
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ services:
- SENEC_HOST
- SENEC_SCHEMA
- CHARGER_INTERVAL
- CHARGER_PRICE_MODE
- CHARGER_PRICE_TIME_RANGE
- CHARGER_FORECAST_THRESHOLD
- INFLUX_HOST=influxdb
- INFLUX_TOKEN=${INFLUX_TOKEN_READ}
- INFLUX_ORG
Expand Down
2 changes: 1 addition & 1 deletion test/cassettes/prices_success.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 35 additions & 1 deletion test/config_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ class ConfigTest < Minitest::Test
VALID_OPTIONS = {
senec_host: '192.168.1.2',
charger_interval: 1800,
charger_price_time_range: 3,
charger_price_mode: :relaxed,
charger_forecast_threshold: 15,
senec_schema: 'http',
influx_host: 'influx.example.com',
influx_schema: 'https',
Expand All @@ -19,16 +22,20 @@ def test_valid_options
Config.new(VALID_OPTIONS)
end

def test_invalid_options
def test_invalid_options_blank
assert_raises(Exception) { Config.new({}) }
end

def test_invalid_options_charger_interval
error =
assert_raises(Exception) do
Config.new(VALID_OPTIONS.merge(charger_interval: 0))
end

assert_match(/Interval is invalid/, error.message)
end

def test_invalid_options_influx_schema
error =
assert_raises(Exception) do
Config.new(VALID_OPTIONS.merge(influx_schema: 'foo'))
Expand All @@ -37,6 +44,33 @@ def test_invalid_options
assert_match(/URL is invalid/, error.message)
end

def test_invalid_options_price_mode
error =
assert_raises(Exception) do
Config.new(VALID_OPTIONS.merge(charger_price_mode: 'foo'))
end

assert_match(/Price mode is invalid/, error.message)
end

def test_invalid_options_price_time_range
error =
assert_raises(Exception) do
Config.new(VALID_OPTIONS.merge(charger_price_time_range: '-2'))
end

assert_match(/Time range is invalid/, error.message)
end

def test_invalid_options_forecast_threshold
error =
assert_raises(Exception) do
Config.new(VALID_OPTIONS.merge(charger_forecast_threshold: '-20'))
end

assert_match(/Forecast threshold is invalid/, error.message)
end

def test_senec_methods
config = Config.new(VALID_OPTIONS)

Expand Down
28 changes: 21 additions & 7 deletions test/prices_provider_test.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
require 'test_helper'

class PricesProviderTest < Minitest::Test
def test_success_request
def test_levels
VCR.use_cassette('prices_success') do
assert_equal 4, prices_provider.levels.size
prices_provider.levels.each do |level|
assert_includes %w[NORMAL CHEAP VERY_CHEAP EXPENSIVE VERY_EXPENSIVE],
level
assert_equal %w[CHEAP CHEAP CHEAP VERY_CHEAP], prices_provider.levels
end
end

def test_cheapest_grid_power_strict
config.stub :charger_price_mode, :strict do
VCR.use_cassette('prices_success') do
refute_predicate prices_provider, :cheap_grid_power?
end
end
end

refute_predicate prices_provider, :cheap_grid_power?
def test_cheapest_grid_power_relaxed
config.stub :charger_price_mode, :relaxed do
VCR.use_cassette('prices_success') do
assert_predicate prices_provider, :cheap_grid_power?
end
end
end

private

def prices_provider
@prices_provider ||= PricesProvider.new(config: Config.from_env)
@prices_provider ||= PricesProvider.new(config:)
end

def config
@config ||= Config.from_env
end
end

0 comments on commit d13a3c9

Please sign in to comment.