Skip to content
Draft
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
1 change: 1 addition & 0 deletions bundles/org.openhab.binding.energidataservice/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ All channels are available for thing type `service`.
| --------------------- | ------- | -------------------------------------------------------------------- | ------------- | -------- |
| priceArea | text | Price area for spot prices (same as bidding zone) | | yes |
| currencyCode | text | Currency code in which to obtain spot prices | DKK | no |
| hourlySpotPrices | boolean | Recalculate spot prices to hourly average based on quarter hourly | false | no |
| gridCompanyGLN | integer | Global Location Number of the Grid Company | | no |
| energinetGLN | integer | Global Location Number of Energinet | 5790000432752 | no |
| reducedElectricityTax | boolean | Reduced electricity tax applies. For electric heating customers only | false | no |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public class EnergiDataServiceConfiguration {
*/
public String gridCompanyGLN = "";

/**
* Recalculate spot prices to hourly average based on quarter hourly
*/
public boolean hourlySpotPrices = false;

/**
* Global Location Number of Energinet.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ private void unsubscribe(Subscription subscription) {

private Subscription getChannelSubscription(String channelId) {
if (CHANNEL_SPOT_PRICE.equals(channelId)) {
return SpotPriceSubscription.of(config.priceArea, config.getCurrency());
return SpotPriceSubscription.of(config.priceArea, config.getCurrency(), config.hourlySpotPrices);
} else if (CHANNEL_CO2_EMISSION_PROGNOSIS.equals(channelId)) {
return Co2EmissionSubscription.of(config.priceArea, Co2EmissionSubscription.Type.Prognosis);
} else if (CHANNEL_CO2_EMISSION_REALTIME.equals(channelId)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import static org.openhab.binding.energidataservice.internal.EnergiDataServiceBindingConstants.*;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
Expand All @@ -23,11 +24,15 @@
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.jdt.annotation.NonNullByDefault;
Expand Down Expand Up @@ -125,7 +130,29 @@ public void subscribe(ElectricityPriceListener listener, Subscription subscripti
public void unsubscribe(ElectricityPriceListener listener, Subscription subscription) {
boolean isLastDistinctSubscription = unsubscribeInternal(listener, subscription);
if (isLastDistinctSubscription) {
subscriptionDataCaches.remove(subscription);
boolean hasOtherSubscription = false;
if (subscription instanceof SpotPriceSubscription spotPriceSubscription
&& !spotPriceSubscription.isHourlyAverage()
&& subscriptionToListeners.containsKey(SpotPriceSubscription
.of(spotPriceSubscription.getPriceArea(), spotPriceSubscription.getCurrency(), true))) {
hasOtherSubscription = true;
logger.trace("Not removing cache for {}, another subscription depends on it.", subscription);
}

if (!hasOtherSubscription) {
logger.trace("Removing cache for {}, no remaining subscriptions depend on it.", subscription);
subscriptionDataCaches.remove(subscription);
}

if (subscription instanceof SpotPriceSubscription spotPriceSubscription
&& spotPriceSubscription.isHourlyAverage()) {
Subscription rawSubscription = SpotPriceSubscription.of(spotPriceSubscription.getPriceArea(),
spotPriceSubscription.getCurrency(), false);
if (!subscriptionToListeners.containsKey(rawSubscription)) {
logger.trace("Removing cache for {}, no remaining subscriptions depend on it.", rawSubscription);
subscriptionDataCaches.remove(rawSubscription);
}
}
}

if (subscriptionToListeners.isEmpty()) {
Expand Down Expand Up @@ -282,10 +309,21 @@ private boolean downloadSpotPricesIfNotCached(SpotPriceSubscription subscription
SpotPriceSubscriptionCache cache = getSpotPriceSubscriptionDataCache(subscription);

if (cache.arePricesFullyCached()) {
logger.debug("Cached spot prices still valid, skipping download.");
logger.debug("Cached spot prices still valid for {}, skipping download.", subscription);
return false;
}

if (subscription.isHourlyAverage()) {
SpotPriceSubscription rawSubscription = SpotPriceSubscription.of(subscription.getPriceArea(),
subscription.getCurrency(), false);
SpotPriceSubscriptionCache rawCache = getSpotPriceSubscriptionDataCache(rawSubscription);
if (rawCache.arePricesFullyCached()) {
logger.debug("Recalculating cached spot prices for {}, skipping download.", subscription);
cache.put(calculateHourlyAverages(rawCache.get()));
return false;
}
}

DateQueryParameter start;
if (cache.areHistoricPricesCached()) {
start = DateQueryParameter.of(DateQueryParameterType.UTC_NOW);
Expand All @@ -309,16 +347,54 @@ private boolean downloadSpotPrices(SpotPriceSubscription subscription, DateQuery
subscription.getCurrency(), start, DateQueryParameter.EMPTY, properties);
isUpdated = cache.put(spotPriceRecords);
} else {
SpotPriceSubscription rawSubscription = SpotPriceSubscription.of(subscription.getPriceArea(),
subscription.getCurrency(), false);
SpotPriceSubscriptionCache rawCache = getSpotPriceSubscriptionDataCache(rawSubscription);
DayAheadPriceRecord[] dayAheadRecords = apiController.getDayAheadPrices(subscription.getPriceArea(),
subscription.getCurrency(), start, DateQueryParameter.EMPTY, properties);
isUpdated = cache.put(dayAheadRecords);
isUpdated = rawCache.put(dayAheadRecords);
if (subscription.isHourlyAverage()) {
logger.debug("Recalculating spot prices for {}, after downloading day-ahead prices.", subscription);
isUpdated = cache.put(calculateHourlyAverages(rawCache.get()));
}
}
} finally {
listenerToSubscriptions.keySet().forEach(listener -> listener.onPropertiesUpdated(properties));
}
return isUpdated;
}

private static Map<Instant, BigDecimal> calculateHourlyAverages(Map<Instant, BigDecimal> quarterHourlyPrices) {
Map<Instant, List<BigDecimal>> groupedByHour = quarterHourlyPrices.entrySet().stream()
.collect(Collectors.groupingBy(e -> e.getKey().truncatedTo(ChronoUnit.HOURS), TreeMap::new,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())));

Map<Instant, BigDecimal> hourlyAverages = new TreeMap<>();

for (Map.Entry<Instant, List<BigDecimal>> entry : groupedByHour.entrySet()) {
List<BigDecimal> prices = entry.getValue();

// Expect exactly 4 quarter-hour values
if (prices.size() == 4) {
BigDecimal avg = average(prices);
if (avg != null) {
hourlyAverages.put(entry.getKey(), avg);
}
}
}

return hourlyAverages;
}

private static @Nullable BigDecimal average(List<@Nullable BigDecimal> values) {
List<BigDecimal> nonNulls = values.stream().filter(Objects::nonNull).toList();
if (nonNulls.size() != 4) {
return null;
}
BigDecimal sum = nonNulls.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(BigDecimal.valueOf(nonNulls.size()), RoundingMode.HALF_UP);
}

private boolean downloadTariffsIfNotCached(DatahubPriceSubscription subscription)
throws InterruptedException, DataServiceException {
GlobalLocationNumber globalLocationNumber = subscription.getGlobalLocationNumber();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ public void flush() {
priceMap.entrySet().removeIf(entry -> entry.getKey().isBefore(firstHourStart));
}

@Override
public boolean put(Map<Instant, BigDecimal> records) {
if (priceMap.equals(records)) {
return false;
}

priceMap.clear();
priceMap.putAll(records);
return true;
}

/**
* Get map of all cached prices.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ public interface SubscriptionDataCache<R> {
*/
boolean put(R records);

/**
* Add key/value pairs to cache.
*
* @param records Records to add to cache
* @return true if the provided records resulted in any cache changes
*/
boolean put(Map<Instant, BigDecimal> records);

/**
* Get cached prices.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
public class SpotPriceSubscription implements ElectricityPriceSubscription {
private final String priceArea;
private final Currency currency;
private final boolean hourlyAverage;

private SpotPriceSubscription(String priceArea, Currency currency) {
private SpotPriceSubscription(String priceArea, Currency currency, boolean hourlyAverage) {
this.priceArea = priceArea;
this.currency = currency;
this.hourlyAverage = hourlyAverage;
}

@Override
Expand All @@ -42,7 +44,8 @@ public boolean equals(@Nullable Object o) {
return false;
}

return this.priceArea.equals(other.priceArea) && this.currency.equals(other.currency);
return this.priceArea.equals(other.priceArea) && this.currency.equals(other.currency)
&& this.hourlyAverage == other.hourlyAverage;
}

@Override
Expand All @@ -52,7 +55,8 @@ public int hashCode() {

@Override
public String toString() {
return "SpotPriceSubscription: PriceArea=" + priceArea + ", Currency=" + currency;
return "SpotPriceSubscription: PriceArea=" + priceArea + ", Currency=" + currency + ", HourlyAverage="
+ hourlyAverage;
}

public String getPriceArea() {
Expand All @@ -63,7 +67,15 @@ public Currency getCurrency() {
return currency;
}

public boolean isHourlyAverage() {
return hourlyAverage;
}

public static SpotPriceSubscription of(String priceArea, Currency currency) {
return new SpotPriceSubscription(priceArea, currency);
return new SpotPriceSubscription(priceArea, currency, false);
}

public static SpotPriceSubscription of(String priceArea, Currency currency, boolean hourlySpotPrices) {
return new SpotPriceSubscription(priceArea, currency, hourlySpotPrices);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
<option value="EUR">Euro</option>
</options>
</parameter>
<parameter name="hourlySpotPrices" type="boolean">
<label>Hourly Spot Prices</label>
<description>Recalculate spot prices to hourly average based on quarter hourly.</description>
<default>false</default>
</parameter>
<parameter name="gridCompanyGLN" type="text">
<label>Grid Company GLN</label>
<description>Global Location Number of the grid company.</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000706686
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001088217 = Veksel
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790000610976 = Vores Elnet
thing-type.config.energidataservice.service.gridCompanyGLN.option.5790001089375 = Zeanet
thing-type.config.energidataservice.service.hourlySpotPrices.label = Hourly Spot Prices
thing-type.config.energidataservice.service.hourlySpotPrices.description = Recalculate spot prices to hourly average based on quarter hourly.
thing-type.config.energidataservice.service.priceArea.label = Price Area
thing-type.config.energidataservice.service.priceArea.description = Price area for spot prices (same as bidding zone).
thing-type.config.energidataservice.service.priceArea.option.DK1 = West of the Great Belt
Expand Down