Skip to content

Commit

Permalink
Add solcast support (#322)
Browse files Browse the repository at this point in the history
* add solcast support

* Fix typo

* Fix solcast with single site

* Prefer "case" over "if"

* Use .env.test.local to record solcast response

* Cleanup Loop class, add test for 100% test coverage

* Fix tests by adding fake solcast config

---------

Co-authored-by: Georg Ledermann <[email protected]>
  • Loading branch information
gereons and ledermann authored May 14, 2024
1 parent af77206 commit 8bd7a99
Show file tree
Hide file tree
Showing 19 changed files with 382 additions and 77 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Forecast.solar configuration
# http://doc.forecast.solar/api:estimate

# see .env.solcast.example for details on using solcast.com.au

# latitude of location, -90 (south) … 90 (north)
FORECAST_LATITUDE=50.123

Expand Down
29 changes: 29 additions & 0 deletions .env.solcast.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# solcast.com configuration
# https://toolkit.solcast.com.au/

# first, create a free account on https://toolkit.solcast.com.au/ and create a "site" for your PV system
# up to two strings are supported, both will be assigned an ID that looks like "1234-5678-9abc-def0"

# use solcast
FORECAST_PROVIDER=solcast

# copy api key from solcast account
SOLCAST_APIKEY=secret-solcast-api-key
SOLCAST_SITE=1111-2222-3333-4444

# Update interval in seconds (beware of the rate-limit!)
FORECAST_INTERVAL=8640

# for multiple strings:
# FORECAST_CONFIGURATIONS=2
# SOLCAST_0_SITE=1111-2222-3333-4444
# SOLCAST_1_SITE=5555-6666-7777-8888

# InfluxDB configuration
INFLUX_HOST=eu-central-1-1.aws.cloud2.influxdata.com
INFLUX_SCHEMA=https
INFLUX_PORT=443
INFLUX_TOKEN=the-secret-token-from-influxdata
[email protected]
INFLUX_BUCKET=my-bucket-name
INFLUX_MEASUREMENT=Forecast
3 changes: 3 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ INFLUX_TOKEN=my-token
INFLUX_ORG=my-org
INFLUX_BUCKET=my-bucket
INFLUX_MEASUREMENT=my-forecast

SOLCAST_APIKEY=fake-api-key
SOLCAST_0_SITE=111-2222-3333-4444
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
.env
.env*
coverage/
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@

# Forecast collector

Collect solar forecast data from https://forecast.solar and push it to InfluxDB.
Collect solar forecast data from https://forecast.solar or https://solcast.com.au and push it to InfluxDB.

## Usage

1. Make sure your InfluxDB database is ready (not subject of this README)

2. Prepare an `.env` file (see `.env.example`) with your InfluxDB credentials and some details about your PV plant (Geo location, azimuth, declination etc.)

You find details about the params on the [forecast.solar API documentation](https://doc.forecast.solar/api:estimate).
Find details about the underlying APIs here:
* [forecast.solar API documentation](https://doc.forecast.solar/api:estimate).
* [solcast API documentation](https://docs.solcast.com.au/) in the Legacy/Hobbyist section

3. Run the Docker container on your Linux box:

Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ require 'rubygems'
require 'bundler'
require 'rake/testtask'
require 'dotenv'
Dotenv.load('.env.test')
Dotenv.load('.env.test.local', '.env.test')

Rake::TestTask.new :test do |t|
t.libs << 'test' << 'app'
Expand Down
38 changes: 38 additions & 0 deletions app/config.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
Config =
Struct.new(
:forecast_provider,
:forecast_configurations,
:forecast_solar_apikey,
:forecast_interval,
:solcast_configurations,
:solcast_apikey,
:influx_schema,
:influx_host,
:influx_port,
Expand Down Expand Up @@ -41,6 +44,7 @@ def validate_url!(url)
def self.from_env(options = {})
new(
{}.merge(forecast_settings_from_env)
.merge(solcast_settings_from_env)
.merge(influx_credentials_from_env)
.merge(options),
)
Expand All @@ -58,9 +62,18 @@ def self.influx_credentials_from_env
}
end

def self.solcast_settings_from_env
defaults = single_solcast_settings_from_env
{
solcast_configurations: all_solcast_settings_from_env(defaults),
solcast_apikey: ENV.fetch('SOLCAST_APIKEY', nil),
}
end

def self.forecast_settings_from_env
defaults = single_forecast_settings_from_env
{
forecast_provider: ENV.fetch('FORECAST_PROVIDER', 'forecast.solar'),
forecast_configurations: all_forecast_settings_from_env(defaults),
forecast_interval: ENV.fetch('FORECAST_INTERVAL').to_i,
forecast_solar_apikey: ENV.fetch('FORECAST_SOLAR_APIKEY', nil),
Expand All @@ -79,13 +92,27 @@ def self.single_forecast_settings_from_env
}
end

def self.single_solcast_settings_from_env
{
solcast_site: ENV.fetch('SOLCAST_SITE', ''),
}
end

def self.all_forecast_settings_from_env(defaults)
config_count = ENV.fetch('FORECAST_CONFIGURATIONS', '1').to_i

(0...config_count).map do |index|
ForecastConfiguration.from_env(index, defaults)
end
end

def self.all_solcast_settings_from_env(defaults)
config_count = ENV.fetch('FORECAST_CONFIGURATIONS', '1').to_i

(0...config_count).map do |index|
SolcastConfiguration.from_env(index, defaults)
end
end
end

ForecastConfiguration =
Expand Down Expand Up @@ -120,3 +147,14 @@ def self.from_env(index, defaults)
}
end
end

SolcastConfiguration =
Struct.new(
:site,
) do
def self.from_env(index, defaults)
{
site: ENV.fetch("SOLCAST_#{index}_SITE", defaults[:solcast_site]),
}
end
end
80 changes: 18 additions & 62 deletions app/forecast.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
require_relative 'config'

class Forecast
BASE_URL = 'https://api.forecast.solar'.freeze

def initialize(config:)
@config = config
end
Expand All @@ -13,9 +11,16 @@ def initialize(config:)
def fetch_data
hashes = []

(0...config.forecast_configurations.length).each do |index|
print " #{index}: #{uri(index)} ... "
len =
case config.forecast_provider
when 'forecast.solar'
config.forecast_configurations.length
when 'solcast'
config.solcast_configurations.length
end

(0...len).each do |index|
print " #{index}: #{uri(index)} ... "
begin
hashes.append(current(index))
puts 'OK'
Expand All @@ -27,53 +32,16 @@ def fetch_data
accumulate(hashes)
end

private

def current(index)
# Change mapping:
# "result": {
# "watts": {
# "1632979620": 0,
# "1632984240": 28,
# "1632988800": 119,
# .....
# =>
# { 1632979620 => 0, 1632980640 => 28, 1632981600 => 119, ... }

forecast_response(index).dig('result', 'watts').transform_keys(&:to_i)
end

def uri(index)
URI.parse(formatted_url(index))
end

def base_url
[BASE_URL, config.forecast_solar_apikey, 'estimate'].compact.join('/')
end

def raw_url
"#{base_url}/:lat/:lon/:dec/:az/:kwp" \
'?damping=:damping_morning,:damping_evening' \
'&time=seconds'
end

def parameters(index)
cfg = config.forecast_configurations[index]
{
lat: cfg[:latitude],
lon: cfg[:longitude],
dec: cfg[:declination],
az: cfg[:azimuth],
kwp: cfg[:kwp],
damping_morning: cfg[:damping_morning],
damping_evening: cfg[:damping_evening],
}
end

def formatted_url(index)
raw_url.tap do |url|
parameters(index).each { |key, value| url.sub!(":#{key}", value) }
def accumulate(hashes)
result = hashes[0]
(1...hashes.length).each do |index|
hashes[index].each do |k, v|
result[k] ||= 0
result[k] += v
end
end

result
end

def forecast_response(index)
Expand All @@ -86,16 +54,4 @@ def forecast_response(index)
throw "Failure: #{response.value}"
end
end

def accumulate(hashes)
result = hashes[0]
(1...hashes.length).each do |index|
hashes[index].each do |k, v|
result[k] ||= 0
result[k] += v
end
end

result
end
end
56 changes: 56 additions & 0 deletions app/forecast_solar.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
require 'net/http'
require_relative 'config'
require_relative 'forecast'

class ForecastSolar < Forecast
BASE_URL = 'https://api.forecast.solar'.freeze

def uri(index)
URI.parse(formatted_url(index))
end

def current(index)
# Change mapping:
# "result": {
# "watts": {
# "1632979620": 0,
# "1632984240": 28,
# "1632988800": 119,
# .....
# =>
# { 1632979620 => 0, 1632980640 => 28, 1632981600 => 119, ... }

forecast_response(index).dig('result', 'watts').transform_keys(&:to_i)
end

private

def base_url
[BASE_URL, config.forecast_solar_apikey, 'estimate'].compact.join('/')
end

def raw_url
"#{base_url}/:lat/:lon/:dec/:az/:kwp" \
'?damping=:damping_morning,:damping_evening' \
'&time=seconds'
end

def parameters(index)
cfg = config.forecast_configurations[index]
{
lat: cfg[:latitude],
lon: cfg[:longitude],
dec: cfg[:declination],
az: cfg[:azimuth],
kwp: cfg[:kwp],
damping_morning: cfg[:damping_morning],
damping_evening: cfg[:damping_evening],
}
end

def formatted_url(index)
raw_url.tap do |url|
parameters(index).each { |key, value| url.sub!(":#{key}", value) }
end
end
end
20 changes: 17 additions & 3 deletions app/loop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

require_relative 'flux_writer'
require_relative 'forecast'
require_relative 'forecast_solar'
require_relative 'solcast'

class Loop
def initialize(config:)
Expand All @@ -17,14 +19,17 @@ def self.start(config:, max_count: nil)

def start(max_count)
self.count = 0
forecast = Forecast.new(config:)
forecast = make_forecast
loop do
self.count += 1
puts "##{count} Fetching forecast"
now = DateTime.now
puts "##{count} Fetching forecast at #{now}"
push_to_influx(forecast.fetch_data)
break if max_count && count >= max_count

puts " Sleeping for #{config.forecast_interval} seconds ..."
next_request = DateTime.now.to_time + config.forecast_interval
puts " Sleeping for #{config.forecast_interval} seconds (until #{next_request}) ..."

sleep config.forecast_interval
end
end
Expand All @@ -40,4 +45,13 @@ def push_to_influx(data)
FluxWriter.push(config:, data:)
puts 'OK'
end

def make_forecast
case config.forecast_provider
when 'forecast.solar'
ForecastSolar.new(config:)
when 'solcast'
Solcast.new(config:)
end
end
end
9 changes: 8 additions & 1 deletion app/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@
config = Config.from_env

puts "Using Ruby #{RUBY_VERSION} on platform #{RUBY_PLATFORM}"
puts "Pulling from api.forecast.solar every #{config.forecast_interval} seconds"
host = case config.forecast_provider
when 'forecast.solar'
'api.forecast.solar'
when 'solcast'
'api.solcast.com.au'
end

puts "Pulling from #{host} every #{config.forecast_interval} seconds"
puts "Pushing to InfluxDB at #{config.influx_url}, " \
"bucket #{config.influx_bucket}, " \
"measurement #{config.influx_measurement}"
Expand Down
Loading

0 comments on commit 8bd7a99

Please sign in to comment.