]> git.basschouten.com Git - openhab-addons.git/commitdiff
[awattar] Add tests and improve code (#16871)
authorJ-N-K <github@klug.nrw>
Sun, 23 Jun 2024 20:05:20 +0000 (22:05 +0200)
committerGitHub <noreply@github.com>
Sun, 23 Jun 2024 20:05:20 +0000 (22:05 +0200)
* [awattar] add tests

Signed-off-by: Jan N. Klug <github@klug.nrw>
15 files changed:
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceResult.java
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestpriceConfiguration.java
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBindingConstants.java
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBridgeConfiguration.java
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarPrice.java
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarUtil.java
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestpriceHandler.java
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandler.java
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarPriceHandler.java
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/TimeRange.java [new file with mode: 0644]
bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.awattar/src/test/resources/org/openhab/binding/awattar/internal/handler/api_response.json [new file with mode: 0644]

index c53d6af8b761f4af6ace55c0db033218436bb1df..b949fbaa099f40ba649db21dfdabc623bdb40aa2 100644 (file)
@@ -21,7 +21,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
  */
 @NonNullByDefault
 public abstract class AwattarBestPriceResult {
-
     private long start;
     private long end;
 
index 4ac4b77af7db7d6a7ecabb9f29faabed623ee9f9..8ab8d82a353e95a5c9b1f5f0bffb1f942a2f060f 100644 (file)
@@ -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() {
index 527336d23b43d724a1b47d10b7db1cd9dbcac9c8..8a22b2e81637d3ed1fc0d67fbe2e3ea7271c2a29 100644 (file)
@@ -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";
 }
index 27ce96b554cd07d60b9e87d4693b96fe61f593a5..f1f815bc715219ef2d4553e3a6820f59fd21b444 100644 (file)
@@ -21,7 +21,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
  */
 @NonNullByDefault
 public class AwattarBridgeConfiguration {
-
     public double basePrice;
     public double vatPercent;
     public String country = "";
index 0c329ffd2405fa616bfe43b88452590210bf81e5..1318348085b653ec44835009f8e11a6f9eaba6ef 100644 (file)
@@ -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<AwattarPrice> 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();
index 9b9b768a7b62c1337d541471f39f2bf63594d7ec..bf623d24c170b4edb94c924280ade5bb8dc66764 100644 (file)
@@ -29,12 +29,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
  */
 @NonNullByDefault
 public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult {
-
-    private List<AwattarPrice> members;
-    private ZoneId zoneId;
+    private final List<AwattarPrice> 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();
index 41fd05b77254b7632845dbf5159721104cdd6428..fc37dc629130912b0d574dec2c5f7c867f6ac9f8 100644 (file)
  */
 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<AwattarPrice> {
-    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<AwattarPrice> {
 
     @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);
     }
 }
index a4f996c8dcdd91fb01839fd2782308728d0cf743..e6ba28341c0778aca45c298cd5aa4594a1526313 100644 (file)
@@ -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) {
index 091d0c4c7c3bdc6e90d16145cf5fd7a2d5d33a05..4c92b523411eeb89aef2cb1b4a1c9236c805da1e 100644 (file)
@@ -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<AwattarPrice> range = getPriceRange(bridgeHandler, timerange);
+
         if (config.consecutive) {
-            ArrayList<AwattarPrice> 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<AwattarPrice> 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<AwattarPrice> getPriceRange(AwattarBridgeHandler bridgeHandler, Timerange range,
-            Comparator<AwattarPrice> comparator) {
-        ArrayList<AwattarPrice> result = new ArrayList<>();
-        SortedMap<Long, AwattarPrice> priceMap = bridgeHandler.getPriceMap();
-        if (priceMap == null) {
+    private List<AwattarPrice> getPriceRange(AwattarBridgeHandler bridgeHandler, TimeRange range) {
+        List<AwattarPrice> result = new ArrayList<>();
+        SortedSet<AwattarPrice> 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());
     }
 }
index 2cf1749d2879dbedb5e99276df1ee219657c253a..8bd65306efb92b56a647a219a583ef1d76a2790c 100644 (file)
@@ -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 <a href="https://www.awattar.de/services/api" />
  *
  *
  *
@@ -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<Long, AwattarPrice> priceMap;
-    private final int dataRefreshInterval = 60;
+    private @Nullable SortedSet<AwattarPrice> 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<Long, AwattarPrice> 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<AwattarPrice> 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<Long, AwattarPrice> 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<AwattarPrice> 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<Long, AwattarPrice> getPriceMap() {
-        if (priceMap == null) {
+    public synchronized SortedSet<AwattarPrice> getPrices() {
+        if (prices == null) {
             refresh();
         }
-        return priceMap;
+        return prices;
     }
 
-    @Nullable
-    public AwattarPrice getPriceFor(long timestamp) {
-        SortedMap<Long, AwattarPrice> priceMap = getPriceMap();
-        if (priceMap == null) {
-            return null;
-        }
-        if (!containsPriceFor(timestamp)) {
+    public @Nullable AwattarPrice getPriceFor(long timestamp) {
+        SortedSet<AwattarPrice> localPrices = getPrices();
+        if (localPrices == null || !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<AwattarPrice> 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);
-    }
 }
index cb4b8e35f5c22cc1d7411240f73cf1b728f59f56..c232edfcc72ff338ffde2a1d430e147821cc7fac 100644 (file)
@@ -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<ThingTypeUID> 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);
         }
 
index b39778dee70f088c1bb6ec7b874b4d96bf529d0f..4f44bfebc70b4512a9a8d87c2fed70ef203ad0a5 100644 (file)
@@ -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 (file)
index 0000000..7e3bf88
--- /dev/null
@@ -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<TimeRange> {
+    /**
+     * 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 (file)
index 0000000..a0e8d07
--- /dev/null
@@ -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<AwattarPrice> 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<Arguments> 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<String, Object> 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 (file)
index 0000000..8f0fbbe
--- /dev/null
@@ -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