mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[awattar] Add tests and improve code (#16871)
* [awattar] add tests Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
parent
93457feda2
commit
994a6b8f6f
@ -21,7 +21,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AwattarBestPriceResult {
|
||||
|
||||
private long start;
|
||||
private long end;
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AwattarBridgeConfiguration {
|
||||
|
||||
public double basePrice;
|
||||
public double vatPercent;
|
||||
public String country = "";
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
public @Nullable AwattarPrice getPriceFor(long timestamp) {
|
||||
SortedSet<AwattarPrice> 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<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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue
Block a user