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; + } + } +}