diff --git a/io.openems.edge.application/EdgeApp.bndrun b/io.openems.edge.application/EdgeApp.bndrun
index a3dd56769d..d66d756948 100644
--- a/io.openems.edge.application/EdgeApp.bndrun
+++ b/io.openems.edge.application/EdgeApp.bndrun
@@ -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)',\
@@ -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)'
\ No newline at end of file
+ stax2-api;version='[4.2.2,4.2.3)',\
+ io.openems.edge.predictor.pv.weather.forecast;version=snapshot
\ No newline at end of file
diff --git a/io.openems.edge.predictor.pv.weather.forecast/.classpath b/io.openems.edge.predictor.pv.weather.forecast/.classpath
new file mode 100644
index 0000000000..1a2da58b85
--- /dev/null
+++ b/io.openems.edge.predictor.pv.weather.forecast/.classpath
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/io.openems.edge.predictor.pv.weather.forecast/.gitignore b/io.openems.edge.predictor.pv.weather.forecast/.gitignore
new file mode 100644
index 0000000000..1c316c001c
--- /dev/null
+++ b/io.openems.edge.predictor.pv.weather.forecast/.gitignore
@@ -0,0 +1 @@
+/bin_test/
diff --git a/io.openems.edge.predictor.pv.weather.forecast/.project b/io.openems.edge.predictor.pv.weather.forecast/.project
new file mode 100644
index 0000000000..6539d5de16
--- /dev/null
+++ b/io.openems.edge.predictor.pv.weather.forecast/.project
@@ -0,0 +1,23 @@
+
+
+ io.openems.edge.predictor.pv.weather.forecast
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ bndtools.core.bndbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ bndtools.core.bndnature
+
+
diff --git a/io.openems.edge.predictor.pv.weather.forecast/.settings/org.eclipse.core.resources.prefs b/io.openems.edge.predictor.pv.weather.forecast/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000000..fba9f99466
--- /dev/null
+++ b/io.openems.edge.predictor.pv.weather.forecast/.settings/org.eclipse.core.resources.prefs
@@ -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/=UTF-8
+encoding/bnd.bnd=UTF-8
+encoding/readme.adoc=UTF-8
diff --git a/io.openems.edge.predictor.pv.weather.forecast/bnd.bnd b/io.openems.edge.predictor.pv.weather.forecast/bnd.bnd
new file mode 100644
index 0000000000..be7fd6752f
--- /dev/null
+++ b/io.openems.edge.predictor.pv.weather.forecast/bnd.bnd
@@ -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}
diff --git a/io.openems.edge.predictor.pv.weather.forecast/readme.adoc b/io.openems.edge.predictor.pv.weather.forecast/readme.adoc
new file mode 100644
index 0000000000..d2b7444c1f
--- /dev/null
+++ b/io.openems.edge.predictor.pv.weather.forecast/readme.adoc
@@ -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[]]
\ No newline at end of file
diff --git a/io.openems.edge.predictor.pv.weather.forecast/src/io/openems/edge/predictor/weather/forecast/Config.java b/io.openems.edge.predictor.pv.weather.forecast/src/io/openems/edge/predictor/weather/forecast/Config.java
new file mode 100644
index 0000000000..c37f6745bf
--- /dev/null
+++ b/io.openems.edge.predictor.pv.weather.forecast/src/io/openems/edge/predictor/weather/forecast/Config.java
@@ -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}]";
+
+}
diff --git a/io.openems.edge.predictor.pv.weather.forecast/src/io/openems/edge/predictor/weather/forecast/OpenMeteoForecast.java b/io.openems.edge.predictor.pv.weather.forecast/src/io/openems/edge/predictor/weather/forecast/OpenMeteoForecast.java
new file mode 100644
index 0000000000..349ceb155a
--- /dev/null
+++ b/io.openems.edge.predictor.pv.weather.forecast/src/io/openems/edge/predictor/weather/forecast/OpenMeteoForecast.java
@@ -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> 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()));
+ }
+}
diff --git a/io.openems.edge.predictor.pv.weather.forecast/src/io/openems/edge/predictor/weather/forecast/PredictorWeatherForecastModel.java b/io.openems.edge.predictor.pv.weather.forecast/src/io/openems/edge/predictor/weather/forecast/PredictorWeatherForecastModel.java
new file mode 100644
index 0000000000..9221c3f55b
--- /dev/null
+++ b/io.openems.edge.predictor.pv.weather.forecast/src/io/openems/edge/predictor/weather/forecast/PredictorWeatherForecastModel.java
@@ -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;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/io.openems.edge.predictor.pv.weather.forecast/src/io/openems/edge/predictor/weather/forecast/PredictorWeatherForecastModelImpl.java b/io.openems.edge.predictor.pv.weather.forecast/src/io/openems/edge/predictor/weather/forecast/PredictorWeatherForecastModelImpl.java
new file mode 100644
index 0000000000..2e3b9ceade
--- /dev/null
+++ b/io.openems.edge.predictor.pv.weather.forecast/src/io/openems/edge/predictor/weather/forecast/PredictorWeatherForecastModelImpl.java
@@ -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> shortWaveRadiationOpt = openMeteoForecast.getShortWaveRadiation();
+
+ // If we don't have data, return empty prediction
+ if (shortWaveRadiationOpt.isEmpty()) {
+ return Prediction.EMPTY_PREDICTION;
+ }
+
+ List 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;
+ }
+ }
+}