
org.openremote.manager.energy.ForecastWindService Maven / Gradle / Ivy
package org.openremote.manager.energy;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.ws.rs.core.Response;
import org.apache.camel.builder.RouteBuilder;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import org.openremote.container.message.MessageBrokerService;
import org.openremote.manager.asset.AssetProcessingService;
import org.openremote.manager.asset.AssetStorageService;
import org.openremote.manager.datapoint.AssetPredictedDatapointService;
import org.openremote.manager.event.ClientEventService;
import org.openremote.manager.gateway.GatewayService;
import org.openremote.manager.rules.RulesService;
import org.openremote.model.Container;
import org.openremote.model.ContainerService;
import org.openremote.model.PersistenceEvent;
import org.openremote.model.asset.impl.ElectricityProducerAsset;
import org.openremote.model.asset.impl.ElectricityProducerWindAsset;
import org.openremote.model.attribute.AttributeEvent;
import org.openremote.model.query.AssetQuery;
import org.openremote.model.syslog.SyslogCategory;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC;
import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType;
import static org.openremote.container.util.MapAccess.getString;
import static org.openremote.container.web.WebTargetBuilder.createClient;
import static org.openremote.manager.gateway.GatewayService.isNotForGateway;
import static org.openremote.model.syslog.SyslogCategory.DATA;
/**
* Calculates power generation for {@link ElectricityProducerWindAsset}.
*/
public class ForecastWindService extends RouteBuilder implements ContainerService {
protected static class WeatherForecastResponseModel {
protected WeatherForecastModel current;
@JsonProperty("hourly")
protected WeatherForecastModel[] list;
public WeatherForecastModel[] getList() {
return list;
}
}
protected static class WeatherForecastModel {
/**
* Seconds
*/
@JsonProperty("dt")
protected long timestamp;
@JsonProperty("temp")
protected double tempature;
@JsonProperty("humidity")
protected int humidity;
@JsonProperty("wind_speed")
protected double windSpeed;
@JsonProperty("wind_deg")
protected int windDirection;
@JsonProperty("uvi")
protected double uv;
public long getTimestamp() {
return timestamp * 1000;
}
public double getTempature() {
return tempature;
}
public int getHumidity() {
return humidity;
}
public double getWindSpeed() {
return windSpeed;
}
public int getWindDirection() {
return windDirection;
}
public double getUv() {
return uv;
}
}
public static final String OR_OPEN_WEATHER_API_APP_ID = "OR_OPEN_WEATHER_API_APP_ID";
protected static final Logger LOG = SyslogCategory.getLogger(DATA, ForecastWindService.class.getName());
protected static final AtomicReference resteasyClient = new AtomicReference<>();
protected AssetStorageService assetStorageService;
protected AssetProcessingService assetProcessingService;
protected GatewayService gatewayService;
protected AssetPredictedDatapointService assetPredictedDatapointService;
protected ClientEventService clientEventService;
protected ScheduledExecutorService scheduledExecutorService;
protected RulesService rulesService;
private ResteasyWebTarget weatherForecastWebTarget;
private String openWeatherAppId;
private final Map> calculationFutures = new HashMap<>();
@SuppressWarnings("unchecked")
@Override
public void configure() throws Exception {
from(PERSISTENCE_TOPIC)
.routeId("Persistence-ForecastWind")
.filter(isPersistenceEventForEntityType(ElectricityProducerWindAsset.class))
.filter(isNotForGateway(gatewayService))
.process(exchange -> processAssetChange((PersistenceEvent) exchange.getIn().getBody(PersistenceEvent.class)));
}
@Override
public void init(Container container) throws Exception {
assetStorageService = container.getService(AssetStorageService.class);
assetProcessingService = container.getService(AssetProcessingService.class);
gatewayService = container.getService(GatewayService.class);
assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class);
clientEventService = container.getService(ClientEventService.class);
scheduledExecutorService = container.getScheduledExecutor();
rulesService = container.getService(RulesService.class);
openWeatherAppId = getString(container.getConfig(), OR_OPEN_WEATHER_API_APP_ID, null);
}
@Override
public void start(Container container) throws Exception {
if (openWeatherAppId == null) {
LOG.fine("No value found for OR_OPEN_WEATHER_API_APP_ID, ForecastWindService won't start");
return;
}
initClient();
weatherForecastWebTarget = resteasyClient.get()
.target("https://api.openweathermap.org/data/2.5")
.queryParam("units", "metric")
.queryParam("exclude", "minutely,daily,alerts")
.queryParam("appid", openWeatherAppId);
container.getService(MessageBrokerService.class).getContext().addRoutes(this);
// Load all enabled producer wind assets
LOG.fine("Loading producer wind assets...");
List electricityProducerWindAssets = assetStorageService.findAll(
new AssetQuery()
.types(ElectricityProducerWindAsset.class)
)
.stream()
.map(asset -> (ElectricityProducerWindAsset) asset)
.filter(electricityProducerWindAsset -> electricityProducerWindAsset.isIncludeForecastWindService().orElse(false)
&& electricityProducerWindAsset.getLocation().isPresent())
.toList();
LOG.fine("Found includes producer wind asset count = " + electricityProducerWindAssets.size());
electricityProducerWindAssets.forEach(this::startCalculation);
clientEventService.addSubscription(
AttributeEvent.class,
null,
this::processAttributeEvent);
}
@Override
public void stop(Container container) throws Exception {
new ArrayList<>(calculationFutures.keySet()).forEach(this::stopCalculation);
}
protected static void initClient() {
synchronized (resteasyClient) {
if (resteasyClient.get() == null) {
resteasyClient.set(createClient(org.openremote.container.Container.SCHEDULED_EXECUTOR));
}
}
}
protected void processAttributeEvent(AttributeEvent attributeEvent) {
processElectricityProducerWindAssetAttributeEvent(attributeEvent);
}
protected synchronized void processElectricityProducerWindAssetAttributeEvent(AttributeEvent attributeEvent) {
if (ElectricityProducerWindAsset.POWER.getName().equals(attributeEvent.getName())
|| ElectricityProducerWindAsset.POWER_FORECAST.getName().equals(attributeEvent.getName())) {
// These are updated by this service
return;
}
if (attributeEvent.getName().equals(ElectricityProducerWindAsset.INCLUDE_FORECAST_WIND_SERVICE.getName())) {
boolean enabled = (Boolean)attributeEvent.getValue().orElse(false);
if (enabled && calculationFutures.containsKey(attributeEvent.getId())) {
// Nothing to do here
return;
} else if (!enabled && !calculationFutures.containsKey(attributeEvent.getId())) {
// Nothing to do here
return;
}
LOG.fine("Processing producer wind asset attribute event: " + attributeEvent);
stopCalculation(attributeEvent.getId());
// Get latest asset from storage
ElectricityProducerWindAsset asset = (ElectricityProducerWindAsset) assetStorageService.find(attributeEvent.getId());
if (asset != null && asset.isIncludeForecastWindService().orElse(false) && asset.getLocation().isPresent()) {
startCalculation(asset);
}
}
if (attributeEvent.getName().equals(ElectricityProducerWindAsset.SET_ACTUAL_WIND_VALUE_WITH_FORECAST.getName())) {
// Get latest asset from storage
ElectricityProducerWindAsset asset = (ElectricityProducerWindAsset) assetStorageService.find(attributeEvent.getId());
// Check if power is currently zero and set it if power forecast has an value
if (asset.getPower().orElse(0d) == 0d && asset.getPowerForecast().orElse(0d) != 0d) {
assetProcessingService.sendAttributeEvent(new AttributeEvent(asset.getId(), ElectricityProducerWindAsset.POWER, asset.getPowerForecast().orElse(0d)), getClass().getSimpleName());
}
}
}
protected void processAssetChange(PersistenceEvent persistenceEvent) {
LOG.fine("Processing producer wind asset change: " + persistenceEvent);
stopCalculation(persistenceEvent.getEntity().getId());
if (persistenceEvent.getCause() != PersistenceEvent.Cause.DELETE) {
if (persistenceEvent.getEntity().isIncludeForecastWindService().orElse(false)
&& persistenceEvent.getEntity().getLocation().isPresent()) {
startCalculation(persistenceEvent.getEntity());
}
}
}
protected void startCalculation(ElectricityProducerWindAsset electricityProducerWindAsset) {
LOG.fine("Starting calculation for producer wind asset: " + electricityProducerWindAsset);
calculationFutures.put(electricityProducerWindAsset.getId(), scheduledExecutorService.scheduleAtFixedRate(() -> {
processWeatherData(electricityProducerWindAsset);
}, 0, 1, TimeUnit.HOURS));
}
protected void stopCalculation(String electricityProducerWindAssetId) {
ScheduledFuture> scheduledFuture = calculationFutures.remove(electricityProducerWindAssetId);
if (scheduledFuture != null) {
LOG.fine("Stopping calculation for producer wind asset: " + electricityProducerWindAssetId);
scheduledFuture.cancel(false);
}
}
protected void processWeatherData(ElectricityProducerWindAsset electricityProducerWindAsset) {
try (Response response = weatherForecastWebTarget
.path("onecall")
.queryParam("lat", electricityProducerWindAsset.getLocation().get().getY())
.queryParam("lon", electricityProducerWindAsset.getLocation().get().getX())
.request()
.build("GET")
.invoke()) {
if (response != null && response.getStatus() == 200) {
WeatherForecastResponseModel weatherForecastResponseModel = response.readEntity(WeatherForecastResponseModel.class);
double currentPower = calculatePower(electricityProducerWindAsset, weatherForecastResponseModel.current);
assetProcessingService.sendAttributeEvent(new AttributeEvent(electricityProducerWindAsset.getId(), ElectricityProducerAsset.POWER_FORECAST.getName(), -currentPower), getClass().getSimpleName());
if (electricityProducerWindAsset.isSetActualWindValueWithForecast().orElse(false)) {
assetProcessingService.sendAttributeEvent(new AttributeEvent(electricityProducerWindAsset.getId(), ElectricityProducerAsset.POWER.getName(), -currentPower), getClass().getSimpleName());
}
for (WeatherForecastModel weatherForecastModel : weatherForecastResponseModel.getList()) {
double powerForecast = calculatePower(electricityProducerWindAsset, weatherForecastModel);
LocalDateTime timestamp = Instant.ofEpochMilli(weatherForecastModel.getTimestamp()).atZone(ZoneId.systemDefault()).toLocalDateTime();
assetPredictedDatapointService.updateValue(electricityProducerWindAsset.getId(), ElectricityProducerAsset.POWER_FORECAST.getName(), -powerForecast, timestamp);
assetPredictedDatapointService.updateValue(electricityProducerWindAsset.getId(), ElectricityProducerAsset.POWER.getName(), -powerForecast, timestamp);
for (int i = 0; i < 3; i++) {
timestamp = timestamp.plusMinutes(15);
assetPredictedDatapointService.updateValue(electricityProducerWindAsset.getId(), ElectricityProducerAsset.POWER_FORECAST.getName(), -powerForecast, timestamp);
assetPredictedDatapointService.updateValue(electricityProducerWindAsset.getId(), ElectricityProducerAsset.POWER.getName(), -powerForecast, timestamp);
}
}
rulesService.fireDeploymentsWithPredictedDataForAsset(electricityProducerWindAsset.getId());
} else {
StringBuilder message = new StringBuilder("Unknown");
if (response != null) {
message.setLength(0);
message.append("Status ");
message.append(response.getStatus());
message.append(" - ");
message.append(response.readEntity(String.class));
}
LOG.warning("Request failed: " + message);
}
} catch (Throwable e) {
if (e.getCause() != null && e.getCause() instanceof IOException) {
LOG.log(Level.SEVERE, "Exception when requesting openweathermap data", e.getCause());
} else {
LOG.log(Level.SEVERE, "Exception when requesting openweathermap data", e);
}
}
}
protected double calculatePower(ElectricityProducerWindAsset electricityProducerWindAsset, WeatherForecastModel weatherForecastModel) {
double windSpeed = weatherForecastModel.getWindSpeed();
double powerForecast = 0;
double windSpeedMin = electricityProducerWindAsset.getWindSpeedMin().orElse(0d);
double windSpeedMax = electricityProducerWindAsset.getWindSpeedMax().orElse(0d);
double windSpeedReference = electricityProducerWindAsset.getWindSpeedReference().orElse(0d);
double energyExportMax = electricityProducerWindAsset.getPowerExportMax().orElse(0d);
if (windSpeed <= 0 || windSpeed < windSpeedMin) {
powerForecast = 0;
}
if (windSpeedMin <= windSpeed && windSpeed <= windSpeedReference) {
powerForecast = Math.pow((windSpeed / windSpeedReference), 2) * energyExportMax;
}
if (windSpeedReference < windSpeed && windSpeed <= windSpeedMax) {
powerForecast = energyExportMax;
}
if (windSpeed > windSpeedMax) {
powerForecast = 0;
}
if (powerForecast < 0) {
powerForecast = 0;
}
return powerForecast;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy