diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceResult.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceResult.java index c53d6af8b76..b949fbaa099 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceResult.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceResult.java @@ -21,7 +21,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; */ @NonNullByDefault public abstract class AwattarBestPriceResult { - private long start; private long end; diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestpriceConfiguration.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestpriceConfiguration.java index 4ac4b77af7d..8ab8d82a353 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestpriceConfiguration.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestpriceConfiguration.java @@ -21,11 +21,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; */ @NonNullByDefault public class AwattarBestpriceConfiguration { - - public int rangeStart; - public int rangeDuration; - public int length; - public boolean consecutive; + public int rangeStart = 0; + public int rangeDuration = 24; + public int length = 1; + public boolean consecutive = true; @Override public String toString() { diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBindingConstants.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBindingConstants.java index 527336d23b4..8a22b2e8163 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBindingConstants.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBindingConstants.java @@ -14,7 +14,6 @@ package org.openhab.binding.awattar.internal; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; -import org.openhab.core.thing.type.ChannelGroupTypeUID; /** * The {@link AwattarBindingConstants} class defines common constants, which are @@ -24,18 +23,12 @@ import org.openhab.core.thing.type.ChannelGroupTypeUID; */ @NonNullByDefault public class AwattarBindingConstants { - public static final String BINDING_ID = "awattar"; - public static final String API = "api"; // List of all Thing Type UIDs public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge"); public static final ThingTypeUID THING_TYPE_PRICE = new ThingTypeUID(BINDING_ID, "prices"); public static final ThingTypeUID THING_TYPE_BESTPRICE = new ThingTypeUID(BINDING_ID, "bestprice"); - public static final ThingTypeUID THING_TYPE_BESTNEXT = new ThingTypeUID(BINDING_ID, "bestnext"); - - public static final ChannelGroupTypeUID CHANNEL_GROUP_TYPE_HOURLY_PRICES = new ChannelGroupTypeUID(BINDING_ID, - "hourly-prices"); public static final String CHANNEL_GROUP_CURRENT = "current"; @@ -51,7 +44,4 @@ public class AwattarBindingConstants { public static final String CHANNEL_COUNTDOWN = "countdown"; public static final String CHANNEL_REMAINING = "remaining"; public static final String CHANNEL_HOURS = "hours"; - public static final String CHANNEL_DURATION = "rangeDuration"; - public static final String CHANNEL_LOOKUP_HOURS = "lookupHours"; - public static final String CHANNEL_CONSECUTIVE = "consecutive"; } diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBridgeConfiguration.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBridgeConfiguration.java index 27ce96b554c..f1f815bc715 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBridgeConfiguration.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBridgeConfiguration.java @@ -21,7 +21,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; */ @NonNullByDefault public class AwattarBridgeConfiguration { - public double basePrice; public double vatPercent; public String country = ""; diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java index 0c329ffd240..1318348085b 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java @@ -28,11 +28,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; */ @NonNullByDefault public class AwattarConsecutiveBestPriceResult extends AwattarBestPriceResult { - private double priceSum = 0; private int length = 0; - private String hours; - private ZoneId zoneId; + private final String hours; + private final ZoneId zoneId; public AwattarConsecutiveBestPriceResult(List prices, ZoneId zoneId) { super(); @@ -40,14 +39,14 @@ public class AwattarConsecutiveBestPriceResult extends AwattarBestPriceResult { StringBuilder hours = new StringBuilder(); boolean second = false; for (AwattarPrice price : prices) { - priceSum += price.getPrice(); + priceSum += price.netPrice(); length++; - updateStart(price.getStartTimestamp()); - updateEnd(price.getEndTimestamp()); + updateStart(price.timerange().start()); + updateEnd(price.timerange().end()); if (second) { hours.append(','); } - hours.append(getHourFrom(price.getStartTimestamp(), zoneId)); + hours.append(getHourFrom(price.timerange().start(), zoneId)); second = true; } this.hours = hours.toString(); diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java index 9b9b768a7b6..bf623d24c17 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java @@ -29,12 +29,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; */ @NonNullByDefault public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult { - - private List members; - private ZoneId zoneId; + private final List members; + private final ZoneId zoneId; private boolean sorted = true; - public AwattarNonConsecutiveBestPriceResult(int size, ZoneId zoneId) { + public AwattarNonConsecutiveBestPriceResult(ZoneId zoneId) { super(); this.zoneId = zoneId; members = new ArrayList<>(); @@ -43,13 +42,13 @@ public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult public void addMember(AwattarPrice member) { sorted = false; members.add(member); - updateStart(member.getStartTimestamp()); - updateEnd(member.getEndTimestamp()); + updateStart(member.timerange().start()); + updateEnd(member.timerange().end()); } @Override public boolean isActive() { - return members.stream().anyMatch(x -> x.contains(Instant.now().toEpochMilli())); + return members.stream().anyMatch(x -> x.timerange().contains(Instant.now().toEpochMilli())); } @Override @@ -59,12 +58,7 @@ public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult private void sort() { if (!sorted) { - members.sort(new Comparator<>() { - @Override - public int compare(AwattarPrice o1, AwattarPrice o2) { - return Long.compare(o1.getStartTimestamp(), o2.getStartTimestamp()); - } - }); + members.sort(Comparator.comparingLong(p -> p.timerange().start())); } } @@ -77,7 +71,7 @@ public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult if (second) { res.append(','); } - res.append(getHourFrom(price.getStartTimestamp(), zoneId)); + res.append(getHourFrom(price.timerange().start(), zoneId)); second = true; } return res.toString(); diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarPrice.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarPrice.java index 41fd05b7725..fc37dc62913 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarPrice.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarPrice.java @@ -12,63 +12,26 @@ */ package org.openhab.binding.awattar.internal; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; - import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.awattar.internal.handler.TimeRange; /** * Class to store hourly price data. * * @author Wolfgang Klimt - initial contribution + * @author Jan N. Klug - Refactored to record */ @NonNullByDefault -public class AwattarPrice implements Comparable { - private final Double price; - private final long endTimestamp; - private final long startTimestamp; - - private final int hour; - - public AwattarPrice(double price, long startTimestamp, long endTimestamp, ZoneId zoneId) { - this.price = price; - this.endTimestamp = endTimestamp; - this.startTimestamp = startTimestamp; - this.hour = ZonedDateTime.ofInstant(Instant.ofEpochMilli(startTimestamp), zoneId).getHour(); - } - - public long getStartTimestamp() { - return startTimestamp; - } - - public long getEndTimestamp() { - return endTimestamp; - } - - public double getPrice() { - return price; - } +public record AwattarPrice(double netPrice, double grossPrice, double netTotal, double grossTotal, + TimeRange timerange) implements Comparable { @Override public String toString() { - return String.format("(%1$tF %1$tR - %2$tR: %3$.3f)", startTimestamp, endTimestamp, getPrice()); - } - - public int getHour() { - return hour; + return String.format("(%1$tF %1$tR - %2$tR: %3$.3f)", timerange.start(), timerange.end(), netPrice); } @Override public int compareTo(AwattarPrice o) { - return price.compareTo(o.price); - } - - public boolean isBetween(long start, long end) { - return startTimestamp >= start && endTimestamp <= end; - } - - public boolean contains(long timestamp) { - return startTimestamp <= timestamp && endTimestamp > timestamp; + return Double.compare(netPrice, o.netPrice); } } diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarUtil.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarUtil.java index a4f996c8dcd..e6ba28341c0 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarUtil.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarUtil.java @@ -44,7 +44,7 @@ public class AwattarUtil { } public static ZonedDateTime getCalendarForHour(int hour, ZoneId zone) { - return ZonedDateTime.now(zone).truncatedTo(ChronoUnit.DAYS).plus(hour, ChronoUnit.HOURS); + return ZonedDateTime.now(zone).truncatedTo(ChronoUnit.DAYS).plusHours(hour); } public static DateTimeType getDateTimeType(long time, TimeZoneProvider tz) { diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestpriceHandler.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestpriceHandler.java index 091d0c4c7c3..4c92b523411 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestpriceHandler.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestpriceHandler.java @@ -30,10 +30,9 @@ import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.SortedMap; +import java.util.SortedSet; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -67,12 +66,11 @@ import org.slf4j.LoggerFactory; */ @NonNullByDefault public class AwattarBestpriceHandler extends BaseThingHandler { + private static final int THING_REFRESH_INTERVAL = 60; private final Logger logger = LoggerFactory.getLogger(AwattarBestpriceHandler.class); - private final int thingRefreshInterval = 60; - @Nullable - private ScheduledFuture thingRefresher; + private @Nullable ScheduledFuture thingRefresher; private final TimeZoneProvider timeZoneProvider; @@ -85,14 +83,8 @@ public class AwattarBestpriceHandler extends BaseThingHandler { public void initialize() { AwattarBestpriceConfiguration config = getConfigAs(AwattarBestpriceConfiguration.class); - boolean configValid = true; - if (config.length >= config.rangeDuration) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.length.value"); - configValid = false; - } - - if (!configValid) { return; } @@ -104,7 +96,8 @@ public class AwattarBestpriceHandler extends BaseThingHandler { * here */ thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels, - getMillisToNextMinute(1, timeZoneProvider), thingRefreshInterval * 1000, TimeUnit.MILLISECONDS); + getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000, + TimeUnit.MILLISECONDS); } } updateStatus(ThingStatus.UNKNOWN); @@ -138,24 +131,24 @@ public class AwattarBestpriceHandler extends BaseThingHandler { return; } AwattarBridgeHandler bridgeHandler = (AwattarBridgeHandler) bridge.getHandler(); - if (bridgeHandler == null || bridgeHandler.getPriceMap() == null) { + if (bridgeHandler == null || bridgeHandler.getPrices() == null) { logger.debug("No prices available, so can't refresh channel."); // no prices available, can't continue updateState(channelUID, state); return; } AwattarBestpriceConfiguration config = getConfigAs(AwattarBestpriceConfiguration.class); - Timerange timerange = getRange(config.rangeStart, config.rangeDuration, bridgeHandler.getTimeZone()); - if (!(bridgeHandler.containsPriceFor(timerange.start) && bridgeHandler.containsPriceFor(timerange.end))) { + TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, bridgeHandler.getTimeZone()); + if (!(bridgeHandler.containsPriceFor(timerange.start()) && bridgeHandler.containsPriceFor(timerange.end()))) { updateState(channelUID, state); return; } AwattarBestPriceResult result; + List range = getPriceRange(bridgeHandler, timerange); + if (config.consecutive) { - ArrayList range = new ArrayList<>(config.rangeDuration); - range.addAll(getPriceRange(bridgeHandler, timerange, - (o1, o2) -> Long.compare(o1.getStartTimestamp(), o2.getStartTimestamp()))); + range.sort(Comparator.comparing(AwattarPrice::timerange)); AwattarConsecutiveBestPriceResult res = new AwattarConsecutiveBestPriceResult( range.subList(0, config.length), bridgeHandler.getTimeZone()); @@ -168,9 +161,8 @@ public class AwattarBestpriceHandler extends BaseThingHandler { } result = res; } else { - List range = getPriceRange(bridgeHandler, timerange, - (o1, o2) -> Double.compare(o1.getPrice(), o2.getPrice())); - AwattarNonConsecutiveBestPriceResult res = new AwattarNonConsecutiveBestPriceResult(config.length, + range.sort(Comparator.naturalOrder()); + AwattarNonConsecutiveBestPriceResult res = new AwattarNonConsecutiveBestPriceResult( bridgeHandler.getTimeZone()); int ct = 0; for (AwattarPrice price : range) { @@ -223,21 +215,18 @@ public class AwattarBestpriceHandler extends BaseThingHandler { } } - private List getPriceRange(AwattarBridgeHandler bridgeHandler, Timerange range, - Comparator comparator) { - ArrayList result = new ArrayList<>(); - SortedMap priceMap = bridgeHandler.getPriceMap(); - if (priceMap == null) { + private List getPriceRange(AwattarBridgeHandler bridgeHandler, TimeRange range) { + List result = new ArrayList<>(); + SortedSet prices = bridgeHandler.getPrices(); + if (prices == null) { logger.debug("No prices available, can't compute ranges"); return result; } - result.addAll(priceMap.values().stream().filter(x -> x.isBetween(range.start, range.end)) - .collect(Collectors.toSet())); - result.sort(comparator); + result.addAll(prices.stream().filter(x -> range.contains(x.timerange())).toList()); return result; } - private Timerange getRange(int start, int duration, ZoneId zoneId) { + protected TimeRange getRange(int start, int duration, ZoneId zoneId) { ZonedDateTime startCal = getCalendarForHour(start, zoneId); ZonedDateTime endCal = startCal.plusHours(duration); ZonedDateTime now = ZonedDateTime.now(zoneId); @@ -251,16 +240,6 @@ public class AwattarBestpriceHandler extends BaseThingHandler { startCal = startCal.plusDays(1); endCal = endCal.plusDays(1); } - return new Timerange(startCal.toInstant().toEpochMilli(), endCal.toInstant().toEpochMilli()); - } - - private class Timerange { - long start; - long end; - - Timerange(long start, long end) { - this.start = start; - this.end = end; - } + return new TimeRange(startCal.toInstant().toEpochMilli(), endCal.toInstant().toEpochMilli()); } } diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandler.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandler.java index 2cf1749d287..8bd65306efb 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandler.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandler.java @@ -20,8 +20,9 @@ import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.SortedMap; -import java.util.TreeMap; +import java.util.Comparator; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -53,7 +54,7 @@ import com.google.gson.JsonSyntaxException; * The {@link AwattarBridgeHandler} is responsible for retrieving data from the aWATTar API. * * The API provides hourly prices for the current day and, starting from 14:00, hourly prices for the next day. - * Check the documentation at https://www.awattar.de/services/api + * Check the documentation at * * * @@ -61,26 +62,22 @@ import com.google.gson.JsonSyntaxException; */ @NonNullByDefault public class AwattarBridgeHandler extends BaseBridgeHandler { + private static final int DATA_REFRESH_INTERVAL = 60; + private final Logger logger = LoggerFactory.getLogger(AwattarBridgeHandler.class); private final HttpClient httpClient; - @Nullable - private ScheduledFuture dataRefresher; + private @Nullable ScheduledFuture dataRefresher; private static final String URLDE = "https://api.awattar.de/v1/marketdata"; private static final String URLAT = "https://api.awattar.at/v1/marketdata"; private String url; // This cache stores price data for up to two days - @Nullable - private SortedMap priceMap; - private final int dataRefreshInterval = 60; + private @Nullable SortedSet prices; private double vatFactor = 0; - private long lastUpdated = 0; private double basePrice = 0; - private long minTimestamp = 0; - private long maxTimestamp = 0; private ZoneId zone; - private TimeZoneProvider timeZoneProvider; + private final TimeZoneProvider timeZoneProvider; public AwattarBridgeHandler(Bridge thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) { super(thing); @@ -110,7 +107,7 @@ public class AwattarBridgeHandler extends BaseBridgeHandler { return; } - dataRefresher = scheduler.scheduleWithFixedDelay(this::refreshIfNeeded, 0, dataRefreshInterval * 1000, + dataRefresher = scheduler.scheduleWithFixedDelay(this::refreshIfNeeded, 0, DATA_REFRESH_INTERVAL * 1000, TimeUnit.MILLISECONDS); } @@ -121,18 +118,17 @@ public class AwattarBridgeHandler extends BaseBridgeHandler { localRefresher.cancel(true); } dataRefresher = null; - priceMap = null; - lastUpdated = 0; + prices = null; } - public void refreshIfNeeded() { + void refreshIfNeeded() { if (needRefresh()) { refresh(); } updateStatus(ThingStatus.ONLINE); } - private void getPrices() { + private void refresh() { try { // we start one day in the past to cover ranges that already started yesterday ZonedDateTime zdt = LocalDate.now(zone).atStartOfDay(zone).minusDays(1); @@ -151,32 +147,26 @@ public class AwattarBridgeHandler extends BaseBridgeHandler { String content = contentResponse.getContentAsString(); logger.trace("aWATTar API response: status = {}, content = '{}'", httpStatus, content); - switch (httpStatus) { - case OK_200: - Gson gson = new Gson(); - SortedMap result = new TreeMap<>(); - minTimestamp = 0; - maxTimestamp = 0; - AwattarApiData apiData = gson.fromJson(content, AwattarApiData.class); - if (apiData != null) { - for (Datum d : apiData.data) { - result.put(d.startTimestamp, - new AwattarPrice(d.marketprice / 10.0, d.startTimestamp, d.endTimestamp, zone)); - updateMin(d.startTimestamp); - updateMax(d.endTimestamp); - } - priceMap = result; - updateStatus(ThingStatus.ONLINE); - lastUpdated = Instant.now().toEpochMilli(); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/error.invalid.data"); + if (httpStatus == OK_200) { + Gson gson = new Gson(); + SortedSet result = new TreeSet<>(Comparator.comparing(AwattarPrice::timerange)); + AwattarApiData apiData = gson.fromJson(content, AwattarApiData.class); + if (apiData != null) { + for (Datum d : apiData.data) { + double netPrice = d.marketprice / 10.0; + TimeRange timerange = new TimeRange(d.startTimestamp, d.endTimestamp); + result.add(new AwattarPrice(netPrice, netPrice * vatFactor, netPrice + basePrice, + (netPrice + basePrice) * vatFactor, timerange)); } - break; - - default: + prices = result; + updateStatus(ThingStatus.ONLINE); + } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/warn.awattar.statuscode"); + "@text/error.invalid.data"); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/warn.awattar.statuscode"); } } catch (JsonSyntaxException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.json"); @@ -193,27 +183,9 @@ public class AwattarBridgeHandler extends BaseBridgeHandler { if (getThing().getStatus() != ThingStatus.ONLINE) { return true; } - SortedMap localMap = priceMap; - if (localMap == null) { - return true; - } - return localMap.lastKey() < Instant.now().toEpochMilli() + 9 * 3600 * 1000; - } - - private void refresh() { - getPrices(); - } - - public double getVatFactor() { - return vatFactor; - } - - public double getBasePrice() { - return basePrice; - } - - public long getLastUpdated() { - return lastUpdated; + SortedSet localPrices = prices; + return localPrices == null + || localPrices.last().timerange().start() < Instant.now().toEpochMilli() + 9 * 3600 * 1000; } public ZoneId getTimeZone() { @@ -221,32 +193,25 @@ public class AwattarBridgeHandler extends BaseBridgeHandler { } @Nullable - public synchronized SortedMap getPriceMap() { - if (priceMap == null) { + public synchronized SortedSet getPrices() { + if (prices == null) { refresh(); } - return priceMap; + return prices; } - @Nullable - public AwattarPrice getPriceFor(long timestamp) { - SortedMap priceMap = getPriceMap(); - if (priceMap == null) { + public @Nullable AwattarPrice getPriceFor(long timestamp) { + SortedSet localPrices = getPrices(); + if (localPrices == null || !containsPriceFor(timestamp)) { return null; } - if (!containsPriceFor(timestamp)) { - return null; - } - for (AwattarPrice price : priceMap.values()) { - if (timestamp >= price.getStartTimestamp() && timestamp < price.getEndTimestamp()) { - return price; - } - } - return null; + return localPrices.stream().filter(e -> e.timerange().contains(timestamp)).findAny().orElse(null); } public boolean containsPriceFor(long timestamp) { - return minTimestamp <= timestamp && maxTimestamp >= timestamp; + SortedSet localPrices = getPrices(); + return localPrices != null && localPrices.first().timerange().start() <= timestamp + && localPrices.last().timerange().end() > timestamp; } @Override @@ -257,12 +222,4 @@ public class AwattarBridgeHandler extends BaseBridgeHandler { logger.debug("Binding {} only supports refresh command", BINDING_ID); } } - - private void updateMin(long ts) { - minTimestamp = (minTimestamp == 0) ? ts : Math.min(minTimestamp, ts); - } - - private void updateMax(long ts) { - maxTimestamp = (maxTimestamp == 0) ? ts : Math.max(ts, maxTimestamp); - } } diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java index cb4b8e35f5c..c232edfcc72 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java @@ -44,7 +44,7 @@ import org.slf4j.LoggerFactory; @NonNullByDefault @Component(configurationPid = "binding.awattar", service = ThingHandlerFactory.class) public class AwattarHandlerFactory extends BaseThingHandlerFactory { - private Logger logger = LoggerFactory.getLogger(AwattarHandlerFactory.class); + private final Logger logger = LoggerFactory.getLogger(AwattarHandlerFactory.class); private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PRICE, THING_TYPE_BESTPRICE, THING_TYPE_BRIDGE); @@ -69,11 +69,9 @@ public class AwattarHandlerFactory extends BaseThingHandlerFactory { if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { return new AwattarBridgeHandler((Bridge) thing, httpClient, timeZoneProvider); - } - if (THING_TYPE_PRICE.equals(thingTypeUID)) { + } else if (THING_TYPE_PRICE.equals(thingTypeUID)) { return new AwattarPriceHandler(thing, timeZoneProvider); - } - if (THING_TYPE_BESTPRICE.equals(thingTypeUID)) { + } else if (THING_TYPE_BESTPRICE.equals(thingTypeUID)) { return new AwattarBestpriceHandler(thing, timeZoneProvider); } diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarPriceHandler.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarPriceHandler.java index b39778dee70..4f44bfebc70 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarPriceHandler.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarPriceHandler.java @@ -55,11 +55,10 @@ import org.slf4j.LoggerFactory; */ @NonNullByDefault public class AwattarPriceHandler extends BaseThingHandler { - + private static final int THING_REFRESH_INTERVAL = 60; private final Logger logger = LoggerFactory.getLogger(AwattarPriceHandler.class); - private int thingRefreshInterval = 60; - private TimeZoneProvider timeZoneProvider; + private final TimeZoneProvider timeZoneProvider; private @Nullable ScheduledFuture thingRefresher; public AwattarPriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) { @@ -78,7 +77,7 @@ public class AwattarPriceHandler extends BaseThingHandler { /** * Initialize the binding and start the refresh job. - * The refresh job runs once after initialization and afterwards every hour. + * The refresh job runs once after initialization and afterward every hour. */ @Override @@ -91,7 +90,7 @@ public class AwattarPriceHandler extends BaseThingHandler { * here */ thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels, - getMillisToNextMinute(1, timeZoneProvider), thingRefreshInterval * 1000, TimeUnit.MILLISECONDS); + getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL, TimeUnit.SECONDS); } } updateStatus(ThingStatus.UNKNOWN); @@ -141,9 +140,9 @@ public class AwattarPriceHandler extends BaseThingHandler { if (group.equals(CHANNEL_GROUP_CURRENT)) { target = ZonedDateTime.now(bridgeHandler.getTimeZone()); } else if (group.startsWith("today")) { - target = getCalendarForHour(Integer.valueOf(group.substring(5)), bridgeHandler.getTimeZone()); + target = getCalendarForHour(Integer.parseInt(group.substring(5)), bridgeHandler.getTimeZone()); } else if (group.startsWith("tomorrow")) { - target = getCalendarForHour(Integer.valueOf(group.substring(8)), bridgeHandler.getTimeZone()).plusDays(1); + target = getCalendarForHour(Integer.parseInt(group.substring(8)), bridgeHandler.getTimeZone()).plusDays(1); } else { logger.warn("Unsupported channel group {}", group); updateState(channelUID, state); @@ -157,21 +156,20 @@ public class AwattarPriceHandler extends BaseThingHandler { updateState(channelUID, state); return; } - double currentprice = price.getPrice(); String channelId = channelUID.getIdWithoutGroup(); switch (channelId) { case CHANNEL_MARKET_NET: - state = toDecimalType(currentprice); + state = toDecimalType(price.netPrice()); break; case CHANNEL_MARKET_GROSS: - state = toDecimalType(currentprice * bridgeHandler.getVatFactor()); + state = toDecimalType(price.grossPrice()); break; case CHANNEL_TOTAL_NET: - state = toDecimalType(currentprice + bridgeHandler.getBasePrice()); + state = toDecimalType(price.netTotal()); break; case CHANNEL_TOTAL_GROSS: - state = toDecimalType((currentprice + bridgeHandler.getBasePrice()) * bridgeHandler.getVatFactor()); + state = toDecimalType(price.grossTotal()); break; default: logger.warn("Unknown channel id {} for Thing type {}", channelUID, getThing().getThingTypeUID()); diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/TimeRange.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/TimeRange.java new file mode 100644 index 00000000000..7e3bf8815c1 --- /dev/null +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/TimeRange.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.awattar.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link TimeRange} defines a time range (defined by two timestamps) + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public record TimeRange(long start, long end) implements Comparable { + /** + * Check if a given timestamp is in this time range + * + * @param timestamp the timestamp + * @return {@code true} if the timestamp is equal to or greater than {@link #start} and less than {@link #end} + */ + public boolean contains(long timestamp) { + return timestamp >= start && timestamp < end; + } + + /** + * Check if another time range is inside this time range + * + * @param other the other time range + * @return {@code true} if {@link #start} of this time range is the same or before the other time range's + * {@link #start} and this {@link #end} is the same or after the other time range's {@link #end} + */ + public boolean contains(TimeRange other) { + return start <= other.start && end >= other.end; + } + + /** + * Compare two time ranges by their start timestamp + * + * @param o the object to be compared + * @return the result of {@link Long#compare(long, long)} for the {@link #start} timestamps + */ + public int compareTo(TimeRange o) { + return Long.compare(start, o.start); + } +} diff --git a/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerTest.java b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerTest.java new file mode 100644 index 00000000000..a0e8d079c08 --- /dev/null +++ b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerTest.java @@ -0,0 +1,193 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.awattar.internal.handler; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.openhab.binding.awattar.internal.AwattarBindingConstants.*; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.ZoneId; +import java.util.Map; +import java.util.Objects; +import java.util.SortedSet; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.binding.awattar.internal.AwattarBindingConstants; +import org.openhab.binding.awattar.internal.AwattarPrice; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.test.java.JavaTest; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.types.State; + +/** + * The {@link AwattarBridgeHandlerTest} contains tests for the {@link AwattarBridgeHandler} + * + * @author Jan N. Klug - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class AwattarBridgeHandlerTest extends JavaTest { + public static final ThingUID BRIDGE_UID = new ThingUID(AwattarBindingConstants.THING_TYPE_BRIDGE, "testBridge"); + + // bridge mocks + private @Mock @NonNullByDefault({}) Bridge bridgeMock; + private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock; + private @Mock @NonNullByDefault({}) HttpClient httpClientMock; + private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock; + private @Mock @NonNullByDefault({}) Request requestMock; + private @Mock @NonNullByDefault({}) ContentResponse contentResponseMock; + + // best price handler mocks + private @Mock @NonNullByDefault({}) Thing bestpriceMock; + private @Mock @NonNullByDefault({}) ThingHandlerCallback bestPriceCallbackMock; + + private @NonNullByDefault({}) AwattarBridgeHandler bridgeHandler; + + @BeforeEach + public void setUp() throws IOException, ExecutionException, InterruptedException, TimeoutException { + try (InputStream inputStream = AwattarBridgeHandlerTest.class.getResourceAsStream("api_response.json")) { + if (inputStream == null) { + throw new IOException("inputstream is null"); + } + byte[] bytes = inputStream.readAllBytes(); + if (bytes == null) { + throw new IOException("Resulting byte-array empty"); + } + when(contentResponseMock.getContentAsString()).thenReturn(new String(bytes, StandardCharsets.UTF_8)); + } + when(contentResponseMock.getStatus()).thenReturn(HttpStatus.OK_200); + when(httpClientMock.newRequest(anyString())).thenReturn(requestMock); + when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock); + when(requestMock.timeout(10, TimeUnit.SECONDS)).thenReturn(requestMock); + when(requestMock.send()).thenReturn(contentResponseMock); + + when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of("GMT+2")); + + bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock); + bridgeHandler.setCallback(bridgeCallbackMock); + bridgeHandler.refreshIfNeeded(); + + when(bridgeMock.getHandler()).thenReturn(bridgeHandler); + + // other mocks + when(bestpriceMock.getBridgeUID()).thenReturn(BRIDGE_UID); + + when(bestPriceCallbackMock.getBridge(any())).thenReturn(bridgeMock); + when(bestPriceCallbackMock.isChannelLinked(any())).thenReturn(true); + } + + @Test + public void testPricesRetrieval() { + SortedSet prices = bridgeHandler.getPrices(); + + assertThat(prices, hasSize(72)); + + Objects.requireNonNull(prices); + + // check if first and last element are correct + assertThat(prices.first().timerange().start(), is(1718316000000L)); + assertThat(prices.last().timerange().end(), is(1718575200000L)); + } + + @Test + public void testGetPriceForSuccess() { + AwattarPrice price = bridgeHandler.getPriceFor(1718503200000L); + + assertThat(price, is(notNullValue())); + Objects.requireNonNull(price); + assertThat(price.netPrice(), is(closeTo(0.219, 0.001))); + } + + @Test + public void testGetPriceForFail() { + AwattarPrice price = bridgeHandler.getPriceFor(1518503200000L); + + assertThat(price, is(nullValue())); + } + + @Test + public void testContainsPrizeFor() { + assertThat(bridgeHandler.containsPriceFor(1618503200000L), is(false)); + assertThat(bridgeHandler.containsPriceFor(1718503200000L), is(true)); + assertThat(bridgeHandler.containsPriceFor(1818503200000L), is(false)); + } + + public static Stream testBestpriceHandler() { + return Stream.of( // + Arguments.of(1, true, CHANNEL_START, new DateTimeType("2024-06-15T14:00:00.000+0200")), + Arguments.of(1, true, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")), + Arguments.of(1, true, CHANNEL_HOURS, new StringType("14")), + Arguments.of(1, false, CHANNEL_START, new DateTimeType("2024-06-15T14:00:00.000+0200")), + Arguments.of(1, false, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")), + Arguments.of(1, false, CHANNEL_HOURS, new StringType("14")), + Arguments.of(2, true, CHANNEL_START, new DateTimeType("2024-06-15T13:00:00.000+0200")), + Arguments.of(2, true, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")), + Arguments.of(2, true, CHANNEL_HOURS, new StringType("13,14")), + Arguments.of(2, false, CHANNEL_START, new DateTimeType("2024-06-15T13:00:00.000+0200")), + Arguments.of(2, false, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")), + Arguments.of(2, false, CHANNEL_HOURS, new StringType("13,14"))); + } + + @ParameterizedTest + @MethodSource + public void testBestpriceHandler(int length, boolean consecutive, String channelId, State expectedState) { + ThingUID bestPriceUid = new ThingUID(AwattarBindingConstants.THING_TYPE_BESTPRICE, "foo"); + Map config = Map.of("length", length, "consecutive", consecutive); + when(bestpriceMock.getConfiguration()).thenReturn(new Configuration(config)); + + AwattarBestpriceHandler handler = new AwattarBestpriceHandler(bestpriceMock, timeZoneProviderMock) { + @Override + protected TimeRange getRange(int start, int duration, ZoneId zoneId) { + return new TimeRange(1718402400000L, 1718488800000L); + } + }; + + handler.setCallback(bestPriceCallbackMock); + + ChannelUID channelUID = new ChannelUID(bestPriceUid, channelId); + handler.refreshChannel(channelUID); + verify(bestPriceCallbackMock).stateUpdated(channelUID, expectedState); + } +} diff --git a/bundles/org.openhab.binding.awattar/src/test/resources/org/openhab/binding/awattar/internal/handler/api_response.json b/bundles/org.openhab.binding.awattar/src/test/resources/org/openhab/binding/awattar/internal/handler/api_response.json new file mode 100644 index 00000000000..8f0fbbe5135 --- /dev/null +++ b/bundles/org.openhab.binding.awattar/src/test/resources/org/openhab/binding/awattar/internal/handler/api_response.json @@ -0,0 +1,438 @@ +{ + "object": "list", + "data": [ + { + "start_timestamp": 1718316000000, + "end_timestamp": 1718319600000, + "marketprice": 83.13, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718319600000, + "end_timestamp": 1718323200000, + "marketprice": 71.45, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718323200000, + "end_timestamp": 1718326800000, + "marketprice": 63.93, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718326800000, + "end_timestamp": 1718330400000, + "marketprice": 59.53, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718330400000, + "end_timestamp": 1718334000000, + "marketprice": 55.82, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718334000000, + "end_timestamp": 1718337600000, + "marketprice": 64.22, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718337600000, + "end_timestamp": 1718341200000, + "marketprice": 85.01, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718341200000, + "end_timestamp": 1718344800000, + "marketprice": 100.95, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718344800000, + "end_timestamp": 1718348400000, + "marketprice": 104.99, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718348400000, + "end_timestamp": 1718352000000, + "marketprice": 102.54, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718352000000, + "end_timestamp": 1718355600000, + "marketprice": 82.18, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718355600000, + "end_timestamp": 1718359200000, + "marketprice": 68.1, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718359200000, + "end_timestamp": 1718362800000, + "marketprice": 60.88, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718362800000, + "end_timestamp": 1718366400000, + "marketprice": 47.46, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718366400000, + "end_timestamp": 1718370000000, + "marketprice": 40.74, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718370000000, + "end_timestamp": 1718373600000, + "marketprice": 41, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718373600000, + "end_timestamp": 1718377200000, + "marketprice": 60.31, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718377200000, + "end_timestamp": 1718380800000, + "marketprice": 75, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718380800000, + "end_timestamp": 1718384400000, + "marketprice": 90.98, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718384400000, + "end_timestamp": 1718388000000, + "marketprice": 136, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718388000000, + "end_timestamp": 1718391600000, + "marketprice": 127.31, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718391600000, + "end_timestamp": 1718395200000, + "marketprice": 117.12, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718395200000, + "end_timestamp": 1718398800000, + "marketprice": 83.41, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718398800000, + "end_timestamp": 1718402400000, + "marketprice": 59.42, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718402400000, + "end_timestamp": 1718406000000, + "marketprice": 60.68, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718406000000, + "end_timestamp": 1718409600000, + "marketprice": 41.04, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718409600000, + "end_timestamp": 1718413200000, + "marketprice": 29.97, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718413200000, + "end_timestamp": 1718416800000, + "marketprice": 28.86, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718416800000, + "end_timestamp": 1718420400000, + "marketprice": 22.51, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718420400000, + "end_timestamp": 1718424000000, + "marketprice": 10.04, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718424000000, + "end_timestamp": 1718427600000, + "marketprice": 1.54, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718427600000, + "end_timestamp": 1718431200000, + "marketprice": 0.09, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718431200000, + "end_timestamp": 1718434800000, + "marketprice": 0, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718434800000, + "end_timestamp": 1718438400000, + "marketprice": -0.06, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718438400000, + "end_timestamp": 1718442000000, + "marketprice": -10.08, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718442000000, + "end_timestamp": 1718445600000, + "marketprice": -29.04, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718445600000, + "end_timestamp": 1718449200000, + "marketprice": -44.92, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718449200000, + "end_timestamp": 1718452800000, + "marketprice": -65.46, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718452800000, + "end_timestamp": 1718456400000, + "marketprice": -80.01, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718456400000, + "end_timestamp": 1718460000000, + "marketprice": -56.23, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718460000000, + "end_timestamp": 1718463600000, + "marketprice": -29.53, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718463600000, + "end_timestamp": 1718467200000, + "marketprice": -4.84, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718467200000, + "end_timestamp": 1718470800000, + "marketprice": -0.01, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718470800000, + "end_timestamp": 1718474400000, + "marketprice": 40, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718474400000, + "end_timestamp": 1718478000000, + "marketprice": 84.28, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718478000000, + "end_timestamp": 1718481600000, + "marketprice": 79.92, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718481600000, + "end_timestamp": 1718485200000, + "marketprice": 64.3, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718485200000, + "end_timestamp": 1718488800000, + "marketprice": 40.4, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718488800000, + "end_timestamp": 1718492400000, + "marketprice": 24.91, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718492400000, + "end_timestamp": 1718496000000, + "marketprice": 10.36, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718496000000, + "end_timestamp": 1718499600000, + "marketprice": 4.92, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718499600000, + "end_timestamp": 1718503200000, + "marketprice": 2.92, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718503200000, + "end_timestamp": 1718506800000, + "marketprice": 2.19, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718506800000, + "end_timestamp": 1718510400000, + "marketprice": 2.53, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718510400000, + "end_timestamp": 1718514000000, + "marketprice": 2.95, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718514000000, + "end_timestamp": 1718517600000, + "marketprice": 0.69, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718517600000, + "end_timestamp": 1718521200000, + "marketprice": -0.02, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718521200000, + "end_timestamp": 1718524800000, + "marketprice": -1.28, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718524800000, + "end_timestamp": 1718528400000, + "marketprice": -10, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718528400000, + "end_timestamp": 1718532000000, + "marketprice": -13.33, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718532000000, + "end_timestamp": 1718535600000, + "marketprice": -20.01, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718535600000, + "end_timestamp": 1718539200000, + "marketprice": -30.01, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718539200000, + "end_timestamp": 1718542800000, + "marketprice": -35.67, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718542800000, + "end_timestamp": 1718546400000, + "marketprice": -29.04, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718546400000, + "end_timestamp": 1718550000000, + "marketprice": -10.14, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718550000000, + "end_timestamp": 1718553600000, + "marketprice": -2.34, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718553600000, + "end_timestamp": 1718557200000, + "marketprice": 56.22, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718557200000, + "end_timestamp": 1718560800000, + "marketprice": 99.65, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718560800000, + "end_timestamp": 1718564400000, + "marketprice": 119.15, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718564400000, + "end_timestamp": 1718568000000, + "marketprice": 124.28, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718568000000, + "end_timestamp": 1718571600000, + "marketprice": 120.34, + "unit": "Eur/MWh" + }, + { + "start_timestamp": 1718571600000, + "end_timestamp": 1718575200000, + "marketprice": 94.44, + "unit": "Eur/MWh" + } + ], + "url": "/de/v1/marketdata" +} \ No newline at end of file