Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pv forecast openmeteo #2973

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions io.openems.edge.application/EdgeApp.bndrun
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@
bnd.identity;id='io.openems.edge.timeofusetariff.rabotcharge',\
bnd.identity;id='io.openems.edge.timeofusetariff.swisspower',\
bnd.identity;id='io.openems.edge.timeofusetariff.tibber',\

bnd.identity;id='io.openems.edge.predictor.pv.weather.forecast'
-runbundles: \
Java-WebSocket;version='[1.5.4,1.5.5)',\
bcpkix;version='[1.79.0,1.79.1)',\
Expand Down Expand Up @@ -441,4 +441,5 @@
org.owasp.encoder;version='[1.3.1,1.3.2)',\
reactive-streams;version='[1.0.4,1.0.5)',\
rrd4j;version='[3.9.0,3.9.1)',\
stax2-api;version='[4.2.2,4.2.3)'
stax2-api;version='[4.2.2,4.2.3)',\
io.openems.edge.predictor.pv.weather.forecast;version=snapshot
16 changes: 16 additions & 0 deletions io.openems.edge.predictor.pv.weather.forecast/.classpath
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER">
<attributes>
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="aQute.bnd.classpath.container"/>
<classpathentry kind="src" output="bin" path="src"/>
<classpathentry kind="src" output="bin_test" path="test">
<attributes>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="bin"/>
</classpath>
1 change: 1 addition & 0 deletions io.openems.edge.predictor.pv.weather.forecast/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/bin_test/
23 changes: 23 additions & 0 deletions io.openems.edge.predictor.pv.weather.forecast/.project
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>io.openems.edge.predictor.pv.weather.forecast</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>bndtools.core.bndbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>bndtools.core.bndnature</nature>
</natures>
</projectDescription>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
eclipse.preferences.version=1
encoding//src/io/openems/edge/predictor/weather/forecast/Config.java=UTF-8
encoding//src/io/openems/edge/predictor/weather/forecast/PredictorWeatherForecastModelImpl.java=UTF-8
encoding/<project>=UTF-8
encoding/bnd.bnd=UTF-8
encoding/readme.adoc=UTF-8
17 changes: 17 additions & 0 deletions io.openems.edge.predictor.pv.weather.forecast/bnd.bnd
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Bundle-Name: OpenEMS Edge io.openems.edge.predictor.weather.forecast
Bundle-Vendor: OpenEMS Association e.V.
Bundle-License: https://opensource.org/licenses/EPL-2.0
Bundle-Version: 1.0.0.${tstamp}

-buildpath: \
${buildpath},\
com.ghgande.j2mod,\
io.openems.common,\
io.openems.edge.bridge.modbus,\
io.openems.edge.common,\
io.openems.edge.predictor.api,\
io.openems.edge.controller.api,\
io.openems.edge.timedata.api

-testpath: \
${testpath}
3 changes: 3 additions & 0 deletions io.openems.edge.predictor.pv.weather.forecast/readme.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
= io.openems.edge.predictor.weather.forecast

https://github.com/OpenEMS/openems/tree/develop/io.openems.edge.predictor.weather.forecast[Source Code icon:github[]]
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.openems.edge.predictor.weather.forecast;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

import io.openems.edge.predictor.api.prediction.LogVerbosity;

@ObjectClassDefinition(//
name = "PV Predictor- Weather Forecast-Model from Openmeteo", //
description = "PV Production Power Prediction using Openmeteo weather forecast api")
@interface Config {

@AttributeDefinition(name = "Component-ID", description = "Unique ID of this Component")
String id() default "predictor0";

@AttributeDefinition(name = "Alias", description = "Human-readable name of this Component; defaults to Component-ID")
String alias() default "";

@AttributeDefinition(name = "Is enabled?", description = "Is this Component enabled?")
boolean enabled() default true;

@AttributeDefinition(name = "Channel-Addresses", description = "List of Channel-Addresses this Predictor is used for, e.g. '*/ActivePower', '*/ActualPower'")
String[] channelAddresses() default { //
"_sum/ProductionActivePower"};

@AttributeDefinition(name = "Log-Verbosity", description = "The log verbosity.")
LogVerbosity logVerbosity() default LogVerbosity.NONE;

@AttributeDefinition(name = "latitude", description = "Geographic latitude coordinate. Ex. 52.52")
String latitude() default "25.230001";

@AttributeDefinition(name = "latitude", description = "Geographic longitude coordinate. Ex. 13.41")
String longitude() default "15.455001";

@AttributeDefinition(name = "Multiplication Factor", description = "multiplication factor to estimate the PV production power from short wave solar radiation, "
+ "m² is the size of each PV_Panel * number of PV-Panels * Efficiency Factor (can be estimated from historical values , KW)")
double factor() default 1.00 * 10.0 * 0.20;

String webconsole_configurationFactory_nameHint() default "Predictor Weather Forecast-Model [{id}]";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.openems.edge.predictor.weather.forecast;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OpenMeteoForecast {
private static final Logger logger = LoggerFactory.getLogger(OpenMeteoForecast.class);
private String apiUrl;
private JsonObject json;

public OpenMeteoForecast() {
// constructor.
}

/**
* Fetch weather forecast data for the given coordinates.
*
* @param latitude Latitude of the location.
* @param longitude Longitude of the location.
* @throws Exception If fetching the data fails.
*/
public void fetchData(String latitude, String longitude) throws Exception {
this.apiUrl = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude
+ "&longitude=" + longitude
+ "&minutely_15=shortwave_radiation&forecast_days=3";

try (BufferedReader br = new BufferedReader(new InputStreamReader(new URL(apiUrl).openStream()))) {
HttpURLConnection conn = (HttpURLConnection) new URL(apiUrl).openConnection();
conn.setRequestMethod("GET");

if (conn.getResponseCode() != 200) {
throw new RuntimeException("Failed: HTTP error code: " + conn.getResponseCode());
}

StringBuilder response = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
response.append(line);
}

this.json = JsonParser.parseString(response.toString()).getAsJsonObject();
} catch (Exception e) {
logger.error("Error in fetching weather data: ", e);
throw e;
}
}

/**
* Get the shortwave radiation data from the fetched JSON.
*
* @return Optional list of shortwave radiation values.
*
* in the future one can add other factors such as temparature, cloud cover , snow cover etc , each parameters can be fetched individually and used for
* production power calculation
*/
public Optional<List<Double>> getShortWaveRadiation() {
return Optional.ofNullable(json)
.map(j -> j.getAsJsonObject("minutely_15"))
.map(m -> m.getAsJsonArray("shortwave_radiation"))
.map(arr -> IntStream.range(0, arr.size())
.mapToObj(arr::get)
.map(element -> element.getAsDouble())
.collect(Collectors.toList()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.openems.edge.predictor.weather.forecast;

import io.openems.edge.common.channel.Doc;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.predictor.api.prediction.Predictor;

public interface PredictorWeatherForecastModel extends Predictor, OpenemsComponent {

public enum ChannelId implements io.openems.edge.common.channel.ChannelId {
;

private final Doc doc;

private ChannelId(Doc doc) {
this.doc = doc;
}

@Override
public Doc doc() {
return this.doc;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package io.openems.edge.predictor.weather.forecast;

import static io.openems.common.utils.DateUtils.roundDownToQuarter;
import static io.openems.edge.predictor.api.prediction.Prediction.EMPTY_PREDICTION;

import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.SortedMap;
import java.util.stream.Collectors;

import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Sets;
import com.google.gson.JsonElement;

import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.timedata.Resolution;
import io.openems.common.types.ChannelAddress;
import io.openems.common.types.OpenemsType;
import io.openems.edge.common.component.ClockProvider;
import io.openems.edge.common.component.ComponentManager;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.sum.Sum;
import io.openems.edge.common.type.TypeUtils;
import io.openems.edge.controller.api.Controller;
import io.openems.edge.predictor.api.prediction.AbstractPredictor;
import io.openems.edge.predictor.api.prediction.Prediction;
import io.openems.edge.predictor.api.prediction.Predictor;
import io.openems.edge.timedata.api.Timedata;

@Designate(ocd = Config.class, factory = true)
@Component(//
name = "Predictor.PV.WeatherForecastModel", //
immediate = true, //
configurationPolicy = ConfigurationPolicy.REQUIRE //
)
public class PredictorWeatherForecastModelImpl extends AbstractPredictor implements Predictor, OpenemsComponent {

private final Logger log = LoggerFactory.getLogger(PredictorWeatherForecastModelImpl.class);
private double factor; // Factor to multiply with short wave solar radiation to forecast PV production

@Reference
private ComponentManager componentManager;

private Config config;
private OpenMeteoForecast openMeteoForecast; // Service to fetch weather data

public PredictorWeatherForecastModelImpl() throws OpenemsNamedException {
super(OpenemsComponent.ChannelId.values(),
Controller.ChannelId.values(),
PredictorWeatherForecastModel.ChannelId.values());
}

@Activate
private void activate(ComponentContext context, Config config) throws Exception {
this.config = config;
super.activate(context, this.config.id(), this.config.alias(), this.config.enabled(),
this.config.channelAddresses(), this.config.logVerbosity());
}

@Override
@Deactivate
protected void deactivate() {
super.deactivate();
}

@Override
protected ClockProvider getClockProvider() {
return this.componentManager;
}

protected Prediction createNewPrediction(ChannelAddress channelAddress) {
try {
// Fetch latest weather forecast data every 15 minutes
this.openMeteoForecast = new OpenMeteoForecast(); // initialize here
this.openMeteoForecast.fetchData(this.config.latitude(), this.config.longitude()); // Fetch the weather forecast data from API
this.factor = config.factor(); // Factor for calculating PV production

Optional<List<Double>> shortWaveRadiationOpt = openMeteoForecast.getShortWaveRadiation();

// If we don't have data, return empty prediction
if (shortWaveRadiationOpt.isEmpty()) {
return Prediction.EMPTY_PREDICTION;
}

List<Double> shortwaveRadiation = shortWaveRadiationOpt.get();

// If the data size is less than 192, return empty prediction
if (shortwaveRadiation.size() < 192) {
return Prediction.EMPTY_PREDICTION;
}

// Get the current time
ZonedDateTime now = ZonedDateTime.now(this.componentManager.getClock());
ZonedDateTime startOfDay = now.truncatedTo(ChronoUnit.DAYS); // Start of the current day

// Calculate the index corresponding to the current 15-minute interval
int currentIntervalIndex = (int) ChronoUnit.MINUTES.between(startOfDay, now) / 15; // Index of current 15-minute interval

// Ensure the currentIntervalIndex is within bounds
if (currentIntervalIndex >= shortwaveRadiation.size()) {
return Prediction.EMPTY_PREDICTION;
}

// Create an array to store the forecast values for the next 192 intervals (48 hours in 15-minute steps)
var values = new Integer[192];

// Extract data starting from the calculated currentIntervalIndex
for (int i = 0; i < 192; i++) {
int dataIndex = currentIntervalIndex + i; // Get the data index for each 15-minute interval
values[i] = dataIndex < shortwaveRadiation.size() ?
(int) Math.round(shortwaveRadiation.get(dataIndex) * this.factor) : 0; // Forecast PV production based on radiation data
}

// Return the prediction starting from the calculated time
return Prediction.from(startOfDay.plusMinutes(currentIntervalIndex * 15), values);

} catch (Exception e) {
log.error("Error creating prediction: ", e);
return Prediction.EMPTY_PREDICTION;
}
}
}