
org.openremote.manager.asset.ForecastService Maven / Gradle / Ivy
/*
* Copyright 2023, OpenRemote Inc.
*
* See the CONTRIBUTORS.txt file in the distribution for a
* full listing of individual contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package org.openremote.manager.asset;
import jakarta.persistence.TypedQuery;
import org.apache.camel.builder.RouteBuilder;
import org.openremote.container.message.MessageBrokerService;
import org.openremote.container.persistence.PersistenceService;
import org.openremote.container.timer.TimerService;
import org.openremote.manager.datapoint.AssetDatapointService;
import org.openremote.manager.datapoint.AssetPredictedDatapointService;
import org.openremote.manager.gateway.GatewayService;
import org.openremote.model.Container;
import org.openremote.model.ContainerService;
import org.openremote.model.PersistenceEvent;
import org.openremote.model.asset.Asset;
import org.openremote.model.attribute.Attribute;
import org.openremote.model.attribute.AttributeMap;
import org.openremote.model.attribute.AttributeRef;
import org.openremote.model.datapoint.AssetDatapoint;
import org.openremote.model.datapoint.Datapoint;
import org.openremote.model.datapoint.ValueDatapoint;
import org.openremote.model.query.AssetQuery;
import org.openremote.model.query.filter.AttributePredicate;
import org.openremote.model.query.filter.NameValuePredicate;
import org.openremote.model.query.filter.StringPredicate;
import org.openremote.model.value.ForecastConfiguration;
import org.openremote.model.value.ForecastConfigurationWeightedExponentialAverage;
import org.openremote.model.value.MetaItemType;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static java.util.stream.Collectors.toList;
import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC;
import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType;
import static org.openremote.manager.gateway.GatewayService.isNotForGateway;
import static org.openremote.model.attribute.Attribute.getAddedOrModifiedAttributes;
import static org.openremote.model.util.TextUtil.requireNonNullAndNonEmpty;
import static org.openremote.model.value.MetaItemType.FORECAST;
/**
* Calculates forecast values for asset attributes with an attached {@link MetaItemType#FORECAST}
* configuration like {@link ForecastConfigurationWeightedExponentialAverage}.
*/
public class ForecastService extends RouteBuilder implements ContainerService {
private static final Logger LOG = Logger.getLogger(ForecastService.class.getName());
private static long STOP_TIMEOUT = Duration.ofSeconds(5).toMillis();
protected TimerService timerService;
protected GatewayService gatewayService;
protected AssetStorageService assetStorageService;
protected AssetDatapointService assetDatapointService;
protected PersistenceService persistenceService;
protected AssetPredictedDatapointService assetPredictedDatapointService;
protected ScheduledExecutorService scheduledExecutorService;
protected ForecastTaskManager forecastTaskManager = new ForecastTaskManager();
@Override
public void init(Container container) throws Exception {
timerService = container.getService(TimerService.class);
gatewayService = container.getService(GatewayService.class);
assetStorageService = container.getService(AssetStorageService.class);
assetDatapointService = container.getService(AssetDatapointService.class);
persistenceService = container.getService(PersistenceService.class);
assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class);
scheduledExecutorService = container.getScheduledExecutor();
}
@Override
public void start(Container container) throws Exception {
container.getService(MessageBrokerService.class).getContext().addRoutes(this);
LOG.fine("Loading forecast asset attributes...");
List> assets = getForecastAssets();
Set forecastAttributes = assets
.stream()
.flatMap(asset -> asset.getAttributes()
.stream()
.filter(attr -> {
if (attr.hasMeta(FORECAST)) {
Optional forecastConfig = attr.getMetaValue(FORECAST);
return forecastConfig.isPresent() &&
ForecastConfigurationWeightedExponentialAverage.TYPE.equals(forecastConfig.get().getType());
}
return false;
})
.map(attr -> new ForecastAttribute(asset, attr)))
.collect(Collectors.toSet());
LOG.fine("Found forecast asset attributes count = " + forecastAttributes.size());
forecastTaskManager.init(forecastAttributes);
}
@Override
public void stop(Container container) throws Exception {
forecastTaskManager.stop(STOP_TIMEOUT);
}
@SuppressWarnings("unchecked")
@Override
public void configure() throws Exception {
from(PERSISTENCE_TOPIC)
.routeId("Persistence-ForecastConfiguration")
.filter(isPersistenceEventForEntityType(Asset.class))
.filter(isNotForGateway(gatewayService))
.process(exchange -> {
PersistenceEvent> persistenceEvent = (PersistenceEvent>)exchange.getIn().getBody(PersistenceEvent.class);
processAssetChange(persistenceEvent);
});
}
protected List> getForecastAssets() {
return assetStorageService.findAll(
new AssetQuery().attributes(
new AttributePredicate().meta(
new NameValuePredicate(
FORECAST,
new StringPredicate(AssetQuery.Match.CONTAINS, true, "type")
)
)
)
);
}
protected void processAssetChange(PersistenceEvent> persistenceEvent) {
Asset> asset = persistenceEvent.getEntity();
Set forecastAttributes = null;
switch (persistenceEvent.getCause()) {
case CREATE:
forecastAttributes = asset.getAttributes()
.stream()
.filter(attr -> {
if (attr.hasMeta(FORECAST)) {
Optional forecastConfig = attr.getMetaValue(FORECAST);
return forecastConfig.isPresent() &&
ForecastConfigurationWeightedExponentialAverage.TYPE.equals(forecastConfig.get().getType());
}
return false;
})
.map(attr -> new ForecastAttribute(asset, attr))
.collect(Collectors.toSet());
forecastTaskManager.add(forecastAttributes);
break;
case UPDATE:
if (persistenceEvent.getPropertyNames() == null || persistenceEvent.getPropertyNames().indexOf("attributes") < 0) {
return;
}
List> oldAttributes = ((AttributeMap)persistenceEvent.getPreviousState("attributes"))
.stream()
.filter(attr -> attr.hasMeta(FORECAST))
.collect(toList());
List> newAttributes = ((AttributeMap) persistenceEvent.getCurrentState("attributes"))
.stream()
.filter(attr -> attr.hasMeta(FORECAST))
.collect(Collectors.toList());
List> newOrModifiedAttributes = getAddedOrModifiedAttributes(oldAttributes, newAttributes).collect(toList());
Set attributesToDelete = newOrModifiedAttributes
.stream()
.map(attr -> new ForecastAttribute(asset, attr))
.filter(attr -> forecastTaskManager.containsAttribute(attr))
.collect(Collectors.toSet());
attributesToDelete.addAll(oldAttributes
.stream()
.filter(oldAttr -> {
return newAttributes
.stream()
.filter(newAttr -> newAttr.getName().equals(oldAttr.getName()))
.count() == 0;
})
.map(attr -> new ForecastAttribute(asset, attr))
.toList()
);
forecastTaskManager.delete(attributesToDelete);
forecastAttributes = newOrModifiedAttributes
.stream()
.filter(attr -> {
if (attr.hasMeta(FORECAST)) {
Optional forecastConfig = attr.getMetaValue(FORECAST);
return forecastConfig.isPresent() &&
ForecastConfigurationWeightedExponentialAverage.TYPE.equals(forecastConfig.get().getType());
}
return false;
})
.map(attr -> new ForecastAttribute(asset, attr))
.collect(Collectors.toSet());
forecastTaskManager.add(forecastAttributes);
break;
case DELETE:
forecastAttributes = asset.getAttributes()
.stream()
.filter(attr -> attr.hasMeta(FORECAST))
.map(attr -> new ForecastAttribute(asset, attr))
.collect(Collectors.toSet());
forecastTaskManager.delete(forecastAttributes);
break;
}
}
private class ForecastTaskManager {
private static long DELAY_MIN_TO_CANCEL_SAFELY = Duration.ofSeconds(2).toMillis();
private static long DEFAULT_SCHEDULE_DELAY = Duration.ofMinutes(15).toMillis();
protected ScheduledFuture> scheduledFuture;
protected Map nextForecastCalculationMap = new HashMap<>();
protected Set forecastAttributes = new HashSet<>();
public synchronized void init(Set attributes) {
if (attributes == null || attributes.size() == 0) {
return;
}
long now = timerService.getCurrentTimeMillis();
attributes.forEach(attr -> {
if (attr.isValidConfig()) {
attr.setForecastTimestamps(loadForecastTimestampsFromDb(attr.getAttributeRef(), now));
forecastAttributes.add(attr);
}
});
start(now, true);
}
public synchronized void add(Set attributes) {
if (attributes == null || attributes.size() == 0) {
return;
}
attributes.forEach(attr -> {
if (attr.isValidConfig()) {
LOG.fine("Adding asset attribute to forecast calculation service: " + attr.getAttributeRef());
forecastAttributes.add(attr);
}
});
long now = timerService.getCurrentTimeMillis();
if (scheduledFuture != null) {
if (scheduledFuture.getDelay(TimeUnit.MILLISECONDS) > DELAY_MIN_TO_CANCEL_SAFELY) {
scheduledFuture.cancel(false);
scheduledFuture = null;
start(now);
}
} else {
start(now);
}
}
public synchronized void delete(Set attributes) {
attributes.forEach(attr -> delete(attr));
}
public synchronized void delete(ForecastAttribute attribute) {
LOG.fine("Removing asset attribute from forecast calculation service: " + attribute.getAttributeRef());
nextForecastCalculationMap.remove(attribute);
forecastAttributes.remove(attribute);
assetPredictedDatapointService.purgeValues(attribute.getAttributeRef().getId(), attribute.getAttributeRef().getName());
}
public synchronized boolean containsAttribute(ForecastAttribute attribute) {
return forecastAttributes.contains(attribute);
}
public synchronized boolean containsAttribute(AttributeRef attributeRef) {
return forecastAttributes
.stream()
.filter(attr -> attr.getAttributeRef().equals(attributeRef))
.findFirst()
.isPresent();
}
public synchronized ForecastAttribute getAttribute(AttributeRef attributeRef) {
return forecastAttributes
.stream()
.filter(attr -> attr.getAttributeRef().equals(attributeRef))
.findFirst()
.orElse(null);
}
private boolean stop(long timeout) {
long start = timerService.getCurrentTimeMillis();
while (true) {
synchronized (ForecastTaskManager.this) {
if (scheduledFuture == null) {
return true;
} else if (scheduledFuture != null && scheduledFuture.getDelay(TimeUnit.MILLISECONDS) > DELAY_MIN_TO_CANCEL_SAFELY) {
scheduledFuture.cancel(false);
scheduledFuture = null;
return true;
} else {
if (timerService.getCurrentTimeMillis() - start > timeout) {
scheduledFuture.cancel(true);
scheduledFuture = null;
return false;
}
}
}
try {
Thread.currentThread().sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
private synchronized void start(long now) {
start(now, false);
}
private synchronized void start(long now, boolean isServerRestart) {
if (scheduledFuture == null) {
addForecastTimestamps(now, isServerRestart);
updateNextForecastCalculationMap();
scheduleForecastCalculation(now, Optional.empty());
}
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
private void calculateForecasts() {
final long now = timerService.getCurrentTimeMillis();
List attributesToCalculate = new ArrayList<>();
try {
synchronized (ForecastTaskManager.this) {
if (Thread.currentThread().isInterrupted()) {
return;
}
purgeForecastTimestamps(now);
addForecastTimestamps(now, false);
purgeForecastTimestamps(now);
nextForecastCalculationMap.forEach((attribute, nextForecastCalculationTimestamp) -> {
if (nextForecastCalculationTimestamp <= now) {
attributesToCalculate.add(attribute);
}
});
}
attributesToCalculate.forEach(attr -> {
if (Thread.currentThread().isInterrupted()) {
return;
}
List forecastTimestamps = attr.getForecastTimestamps();
if (forecastTimestamps == null || forecastTimestamps.size() == 0) {
return;
}
if (!(attr.getConfig() instanceof ForecastConfigurationWeightedExponentialAverage)) {
return;
}
ForecastConfigurationWeightedExponentialAverage weaConfig = (ForecastConfigurationWeightedExponentialAverage)attr.getConfig();
LOG.fine("Calculating forecast values for attribute: " + attr.getAttributeRef());
Long offset = forecastTimestamps.get(0) - (now + weaConfig.getForecastPeriod().toMillis());
List> allSampleTimestamps = calculateSampleTimestamps(weaConfig, offset);
List historyDatapointBuckets = getHistoryDataFromDb(attr.getAttributeRef(), weaConfig, offset);
List> forecastValues = allSampleTimestamps.stream().map(sampleTimestamps -> {
List sampleDatapoints = findSampleDatapoints(historyDatapointBuckets, sampleTimestamps);
if (sampleDatapoints.size() == weaConfig.getPastCount()) {
return calculateWeightedExponentialAverage(attr.getAttribute(), sampleDatapoints);
} else {
return Optional.empty();
}
}).toList();
if (forecastTimestamps.size() >= forecastValues.size()) {
List> datapoints = IntStream
.range(0, forecastValues.size())
.filter(i -> forecastValues.get(i).isPresent())
.mapToObj(i -> new ValueDatapoint<>(
forecastTimestamps.get(i),
forecastValues.get(i).get())
)
.collect(Collectors.toList());
assetPredictedDatapointService.purgeValues(attr.getId(), attr.getName());
if (datapoints.size() > 0) {
LOG.fine("Updating forecast values for attribute: " + attr.getAttributeRef());
assetPredictedDatapointService.updateValues(attr.getId(), attr.getName(), datapoints);
}
}
});
synchronized (ForecastTaskManager.this) {
if (Thread.currentThread().isInterrupted()) {
return;
}
updateNextForecastCalculationMap();
scheduleForecastCalculation(timerService.getCurrentTimeMillis(), Optional.empty());
}
} catch (Exception e) {
LOG.log(Level.SEVERE, "Exception while calculating and updating forecast values", e);
scheduleForecastCalculation(
timerService.getCurrentTimeMillis(),
Optional.of(DEFAULT_SCHEDULE_DELAY)
);
}
}
private synchronized void scheduleForecastCalculation(long now, Optional fixedDelay) {
Optional delay = fixedDelay;
if (delay.isEmpty()) {
delay = calculateScheduleDelay(now);
}
if (delay.isPresent()) {
LOG.fine("Scheduling next forecast calculation in '" + delay.get() + " [ms]'.");
scheduledFuture = scheduledExecutorService.schedule(() -> calculateForecasts(), delay.get(), TimeUnit.MILLISECONDS);
} else {
scheduledFuture = null;
if (!forecastAttributes.isEmpty()) {
LOG.fine("Scheduling next forecast calculation in '" + DEFAULT_SCHEDULE_DELAY + " [ms]'.");
scheduleForecastCalculation(now, Optional.of(DEFAULT_SCHEDULE_DELAY));
}
}
}
private List> calculateSampleTimestamps(ForecastConfigurationWeightedExponentialAverage config, Long offset) {
List> sampleTimestamps = new ArrayList<>(config.getForecastCount());
long now = timerService.getCurrentTimeMillis();
long pastPeriod = config.getPastPeriod().toMillis();
long forecastPeriod = config.getForecastPeriod().toMillis();
for (int forecastIndex = 1; forecastIndex <= config.getForecastCount(); forecastIndex++) {
List timestamps = new ArrayList<>(config.getPastCount());
for (int pastPeriodIndex = config.getPastCount(); pastPeriodIndex > 0; pastPeriodIndex--) {
timestamps.add(now - (pastPeriod * pastPeriodIndex) + (forecastPeriod * forecastIndex) + offset);
}
sampleTimestamps.add(timestamps);
}
return sampleTimestamps;
}
private List calculateForecastTimestamps(long now, ForecastConfigurationWeightedExponentialAverage config) {
List forecastTimestamps = new ArrayList<>(config.getForecastCount());
long forecastPeriod = config.getForecastPeriod().toMillis();
for (int forecastIndex = 1; forecastIndex <= config.getForecastCount(); forecastIndex++) {
forecastTimestamps.add(now + forecastPeriod * forecastIndex);
}
return forecastTimestamps;
}
private List findSampleDatapoints(List datapointBuckets, List sampleTimestamps) {
List sampleDatapoints = new ArrayList<>(sampleTimestamps.size());
for (Long timestamp : sampleTimestamps) {
AssetDatapoint foundDatapoint = null;
List datapoints = datapointBuckets
.stream()
.filter(bucket -> bucket.isInTimeRange(timestamp))
.findFirst()
.map(bucket -> bucket.getDatapoints())
.orElse(null);
if (datapoints == null) {
continue;
}
for (AssetDatapoint assetDatapoint : datapoints) {
if (assetDatapoint.getTimestamp() <= timestamp) {
foundDatapoint = assetDatapoint;
} else if (assetDatapoint.getTimestamp() > timestamp) {
break;
}
}
if (foundDatapoint != null) {
sampleDatapoints.add(foundDatapoint);
}
}
return sampleDatapoints;
}
private Optional calculateWeightedExponentialAverage(Attribute> attribute, List datapoints) {
// a = 2 / (R + 1)
// p: past period
// Attr(t) = Attr(t-p) * a + Attr(t-2p) * (1 - a)
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy