mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-27 07:41:39 +01:00
[awattarar] Add aWATTar API class (#17169)
Signed-off-by: Thomas Leber <thomas@tl-photography.at> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
b6ce04d6f2
commit
4643c1cca4
@ -20,6 +20,12 @@ import org.openhab.binding.awattar.internal.handler.TimeRange;
|
|||||||
*
|
*
|
||||||
* @author Wolfgang Klimt - initial contribution
|
* @author Wolfgang Klimt - initial contribution
|
||||||
* @author Jan N. Klug - Refactored to record
|
* @author Jan N. Klug - Refactored to record
|
||||||
|
*
|
||||||
|
* @param netPrice the net price in €/kWh
|
||||||
|
* @param grossPrice the gross price in €/kWh
|
||||||
|
* @param netTotal the net total price in €
|
||||||
|
* @param grossTotal the gross total price in €
|
||||||
|
* @param timerange the time range of the price
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public record AwattarPrice(double netPrice, double grossPrice, double netTotal, double grossTotal,
|
public record AwattarPrice(double netPrice, double grossPrice, double netTotal, double grossTotal,
|
||||||
|
@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* 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.api;
|
||||||
|
|
||||||
|
import static org.eclipse.jetty.http.HttpMethod.GET;
|
||||||
|
import static org.eclipse.jetty.http.HttpStatus.OK_200;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
|
import org.eclipse.jetty.client.api.ContentResponse;
|
||||||
|
import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration;
|
||||||
|
import org.openhab.binding.awattar.internal.AwattarPrice;
|
||||||
|
import org.openhab.binding.awattar.internal.dto.AwattarApiData;
|
||||||
|
import org.openhab.binding.awattar.internal.dto.Datum;
|
||||||
|
import org.openhab.binding.awattar.internal.handler.TimeRange;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link AwattarApi} class is responsible for encapsulating the aWATTar API
|
||||||
|
* and providing the data to the bridge.
|
||||||
|
*
|
||||||
|
* @author Thomas Leber - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class AwattarApi {
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(AwattarApi.class);
|
||||||
|
|
||||||
|
private static final String URL_DE = "https://api.awattar.de/v1/marketdata";
|
||||||
|
private static final String URL_AT = "https://api.awattar.at/v1/marketdata";
|
||||||
|
private String url = URL_DE;
|
||||||
|
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
|
private double vatFactor;
|
||||||
|
private double basePrice;
|
||||||
|
|
||||||
|
private ZoneId zone;
|
||||||
|
|
||||||
|
private Gson gson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic exception for the aWATTar API.
|
||||||
|
*/
|
||||||
|
public class AwattarApiException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public AwattarApiException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for the aWATTar API.
|
||||||
|
*
|
||||||
|
* @param httpClient the HTTP client to use
|
||||||
|
* @param zone the time zone to use
|
||||||
|
*/
|
||||||
|
public AwattarApi(HttpClient httpClient, ZoneId zone, AwattarBridgeConfiguration config) {
|
||||||
|
this.zone = zone;
|
||||||
|
this.httpClient = httpClient;
|
||||||
|
|
||||||
|
this.gson = new Gson();
|
||||||
|
|
||||||
|
vatFactor = 1 + (config.vatPercent / 100);
|
||||||
|
basePrice = config.basePrice;
|
||||||
|
|
||||||
|
if (config.country.equals("DE")) {
|
||||||
|
this.url = URL_DE;
|
||||||
|
} else if (config.country.equals("AT")) {
|
||||||
|
this.url = URL_AT;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Country code must be 'DE' or 'AT'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data from the aWATTar API.
|
||||||
|
* The data is returned as a sorted set of {@link AwattarPrice} objects.
|
||||||
|
* The data is requested from now minus one day to now plus three days.
|
||||||
|
*
|
||||||
|
* @return the data as a sorted set of {@link AwattarPrice} objects
|
||||||
|
* @throws AwattarApiException
|
||||||
|
* @throws InterruptedException if the thread is interrupted
|
||||||
|
* @throws TimeoutException if the request times out
|
||||||
|
* @throws ExecutionException if the request fails
|
||||||
|
* @throws EmptyDataResponseException if the response is empty
|
||||||
|
*/
|
||||||
|
public SortedSet<AwattarPrice> getData() throws AwattarApiException {
|
||||||
|
try {
|
||||||
|
// we start one day in the past to cover ranges that already started yesterday
|
||||||
|
ZonedDateTime zdt = LocalDate.now(zone).atStartOfDay(zone).minusDays(1);
|
||||||
|
long start = zdt.toInstant().toEpochMilli();
|
||||||
|
// Starting from midnight yesterday we add three days so that the range covers
|
||||||
|
// the whole next day.
|
||||||
|
zdt = zdt.plusDays(3);
|
||||||
|
long end = zdt.toInstant().toEpochMilli();
|
||||||
|
|
||||||
|
StringBuilder request = new StringBuilder(url);
|
||||||
|
request.append("?start=").append(start).append("&end=").append(end);
|
||||||
|
|
||||||
|
logger.trace("aWATTar API request: = '{}'", request);
|
||||||
|
ContentResponse contentResponse = httpClient.newRequest(request.toString()).method(GET)
|
||||||
|
.timeout(10, TimeUnit.SECONDS).send();
|
||||||
|
int httpStatus = contentResponse.getStatus();
|
||||||
|
String content = contentResponse.getContentAsString();
|
||||||
|
logger.trace("aWATTar API response: status = {}, content = '{}'", httpStatus, content);
|
||||||
|
|
||||||
|
if (content == null) {
|
||||||
|
throw new AwattarApiException("@text/error.empty.data");
|
||||||
|
} else if (httpStatus == OK_200) {
|
||||||
|
SortedSet<AwattarPrice> result = new TreeSet<>(Comparator.comparing(AwattarPrice::timerange));
|
||||||
|
|
||||||
|
AwattarApiData apiData = gson.fromJson(content, AwattarApiData.class);
|
||||||
|
|
||||||
|
for (Datum d : apiData.data) {
|
||||||
|
// the API returns prices in €/MWh, we need €ct/kWh -> divide by 10 (100/1000)
|
||||||
|
double netMarket = d.marketprice / 10.0;
|
||||||
|
double grossMarket = netMarket * vatFactor;
|
||||||
|
double netTotal = netMarket + basePrice;
|
||||||
|
double grossTotal = netTotal * vatFactor;
|
||||||
|
|
||||||
|
result.add(new AwattarPrice(netMarket, grossMarket, netTotal, grossTotal,
|
||||||
|
new TimeRange(d.startTimestamp, d.endTimestamp)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
throw new AwattarApiException("@text/warn.awattar.statuscode" + httpStatus);
|
||||||
|
}
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
throw new AwattarApiException("@text/error.execution");
|
||||||
|
} catch (JsonSyntaxException e) {
|
||||||
|
throw new AwattarApiException("@text/error.json");
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new AwattarApiException("@text/error.interrupted");
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
throw new AwattarApiException("@text/error.timeout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -99,7 +99,7 @@ public class AwattarBestPriceHandler extends BaseThingHandler {
|
|||||||
* here
|
* here
|
||||||
*/
|
*/
|
||||||
thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
|
thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
|
||||||
getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000,
|
getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000L,
|
||||||
TimeUnit.MILLISECONDS);
|
TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,21 +12,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.awattar.internal.handler;
|
package org.openhab.binding.awattar.internal.handler;
|
||||||
|
|
||||||
import static org.eclipse.jetty.http.HttpMethod.GET;
|
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_MARKET_NET;
|
||||||
import static org.eclipse.jetty.http.HttpStatus.OK_200;
|
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_TOTAL_NET;
|
||||||
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.*;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.SortedSet;
|
import java.util.SortedSet;
|
||||||
import java.util.TreeSet;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import javax.measure.Unit;
|
import javax.measure.Unit;
|
||||||
@ -34,11 +28,10 @@ import javax.measure.Unit;
|
|||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
import org.eclipse.jetty.client.api.ContentResponse;
|
|
||||||
import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration;
|
import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration;
|
||||||
import org.openhab.binding.awattar.internal.AwattarPrice;
|
import org.openhab.binding.awattar.internal.AwattarPrice;
|
||||||
import org.openhab.binding.awattar.internal.dto.AwattarApiData;
|
import org.openhab.binding.awattar.internal.api.AwattarApi;
|
||||||
import org.openhab.binding.awattar.internal.dto.Datum;
|
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
|
||||||
import org.openhab.core.i18n.TimeZoneProvider;
|
import org.openhab.core.i18n.TimeZoneProvider;
|
||||||
import org.openhab.core.library.types.QuantityType;
|
import org.openhab.core.library.types.QuantityType;
|
||||||
import org.openhab.core.library.unit.CurrencyUnits;
|
import org.openhab.core.library.unit.CurrencyUnits;
|
||||||
@ -54,13 +47,12 @@ import org.openhab.core.types.util.UnitUtils;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.JsonSyntaxException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link AwattarBridgeHandler} is responsible for retrieving data from the aWATTar API.
|
* The {@link AwattarBridgeHandler} is responsible for retrieving data from the
|
||||||
|
* aWATTar API via the {@link AwattarApi}.
|
||||||
*
|
*
|
||||||
* The API provides hourly prices for the current day and, starting from 14:00, hourly prices for the next day.
|
* The API provides hourly prices for the current day and, starting from 14:00,
|
||||||
|
* hourly prices for the next day.
|
||||||
* Check the documentation at <a href="https://www.awattar.de/services/api" />
|
* Check the documentation at <a href="https://www.awattar.de/services/api" />
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
@ -73,25 +65,19 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
|
|||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(AwattarBridgeHandler.class);
|
private final Logger logger = LoggerFactory.getLogger(AwattarBridgeHandler.class);
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
|
|
||||||
private @Nullable ScheduledFuture<?> dataRefresher;
|
private @Nullable ScheduledFuture<?> dataRefresher;
|
||||||
private Instant lastRefresh = Instant.EPOCH;
|
private Instant lastRefresh = Instant.EPOCH;
|
||||||
|
|
||||||
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
|
// This cache stores price data for up to two days
|
||||||
private @Nullable SortedSet<AwattarPrice> prices;
|
private @Nullable SortedSet<AwattarPrice> prices;
|
||||||
private double vatFactor = 0;
|
|
||||||
private double basePrice = 0;
|
|
||||||
private ZoneId zone;
|
private ZoneId zone;
|
||||||
private final TimeZoneProvider timeZoneProvider;
|
|
||||||
|
private @Nullable AwattarApi awattarApi;
|
||||||
|
|
||||||
public AwattarBridgeHandler(Bridge thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
|
public AwattarBridgeHandler(Bridge thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
|
||||||
super(thing);
|
super(thing);
|
||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
url = URLDE;
|
|
||||||
this.timeZoneProvider = timeZoneProvider;
|
|
||||||
zone = timeZoneProvider.getTimeZone();
|
zone = timeZoneProvider.getTimeZone();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,24 +85,15 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
|
|||||||
public void initialize() {
|
public void initialize() {
|
||||||
updateStatus(ThingStatus.UNKNOWN);
|
updateStatus(ThingStatus.UNKNOWN);
|
||||||
AwattarBridgeConfiguration config = getConfigAs(AwattarBridgeConfiguration.class);
|
AwattarBridgeConfiguration config = getConfigAs(AwattarBridgeConfiguration.class);
|
||||||
vatFactor = 1 + (config.vatPercent / 100);
|
|
||||||
basePrice = config.basePrice;
|
try {
|
||||||
zone = timeZoneProvider.getTimeZone();
|
awattarApi = new AwattarApi(httpClient, zone, config);
|
||||||
switch (config.country) {
|
|
||||||
case "DE":
|
|
||||||
url = URLDE;
|
|
||||||
break;
|
|
||||||
case "AT":
|
|
||||||
url = URLAT;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
|
||||||
"@text/error.unsupported.country");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dataRefresher = scheduler.scheduleWithFixedDelay(this::refreshIfNeeded, 0, DATA_REFRESH_INTERVAL * 1000L,
|
dataRefresher = scheduler.scheduleWithFixedDelay(this::refreshIfNeeded, 0, DATA_REFRESH_INTERVAL * 1000L,
|
||||||
TimeUnit.MILLISECONDS);
|
TimeUnit.MILLISECONDS);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.unsupported.country");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -135,71 +112,36 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data from the API.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
private void refresh() {
|
private void refresh() {
|
||||||
try {
|
try {
|
||||||
// we start one day in the past to cover ranges that already started yesterday
|
// Method is private and only called when dataRefresher is initialized.
|
||||||
ZonedDateTime zdt = LocalDate.now(zone).atStartOfDay(zone).minusDays(1);
|
// DataRefresher is initialized after successful creation of AwattarApi.
|
||||||
long start = zdt.toInstant().toEpochMilli();
|
prices = awattarApi.getData();
|
||||||
// Starting from midnight yesterday we add three days so that the range covers the whole next day.
|
|
||||||
zdt = zdt.plusDays(3);
|
|
||||||
long end = zdt.toInstant().toEpochMilli();
|
|
||||||
|
|
||||||
StringBuilder request = new StringBuilder(url);
|
|
||||||
request.append("?start=").append(start).append("&end=").append(end);
|
|
||||||
|
|
||||||
logger.trace("aWATTar API request: = '{}'", request);
|
|
||||||
ContentResponse contentResponse = httpClient.newRequest(request.toString()).method(GET)
|
|
||||||
.timeout(10, TimeUnit.SECONDS).send();
|
|
||||||
int httpStatus = contentResponse.getStatus();
|
|
||||||
String content = contentResponse.getContentAsString();
|
|
||||||
logger.trace("aWATTar API response: status = {}, content = '{}'", httpStatus, content);
|
|
||||||
|
|
||||||
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) {
|
|
||||||
TimeSeries netMarketSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
|
TimeSeries netMarketSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
|
||||||
TimeSeries netTotalSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
|
TimeSeries netTotalSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
|
||||||
|
|
||||||
Unit<?> priceUnit = getPriceUnit();
|
Unit<?> priceUnit = getPriceUnit();
|
||||||
|
|
||||||
for (Datum d : apiData.data) {
|
for (AwattarPrice price : prices) {
|
||||||
double netMarket = d.marketprice / 10.0;
|
Instant timestamp = Instant.ofEpochMilli(price.timerange().start());
|
||||||
double grossMarket = netMarket * vatFactor;
|
|
||||||
double netTotal = netMarket + basePrice;
|
|
||||||
double grossTotal = netTotal * vatFactor;
|
|
||||||
Instant timestamp = Instant.ofEpochMilli(d.startTimestamp);
|
|
||||||
|
|
||||||
netMarketSeries.add(timestamp, new QuantityType<>(netMarket / 100.0, priceUnit));
|
netMarketSeries.add(timestamp, new QuantityType<>(price.netPrice() / 100.0, priceUnit));
|
||||||
netTotalSeries.add(timestamp, new QuantityType<>(netTotal / 100.0, priceUnit));
|
netTotalSeries.add(timestamp, new QuantityType<>(price.netTotal() / 100.0, priceUnit));
|
||||||
|
|
||||||
result.add(new AwattarPrice(netMarket, grossMarket, netTotal, grossTotal,
|
|
||||||
new TimeRange(d.startTimestamp, d.endTimestamp)));
|
|
||||||
}
|
}
|
||||||
prices = result;
|
|
||||||
|
|
||||||
// update channels
|
// update channels
|
||||||
sendTimeSeries(CHANNEL_MARKET_NET, netMarketSeries);
|
sendTimeSeries(CHANNEL_MARKET_NET, netMarketSeries);
|
||||||
sendTimeSeries(CHANNEL_TOTAL_NET, netTotalSeries);
|
sendTimeSeries(CHANNEL_TOTAL_NET, netTotalSeries);
|
||||||
|
|
||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.ONLINE);
|
||||||
} else {
|
} catch (AwattarApiException e) {
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||||
"@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");
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.interrupted");
|
|
||||||
} catch (ExecutionException e) {
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.execution");
|
|
||||||
} catch (TimeoutException e) {
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.timeout");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,13 +155,13 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createAndSendTimeSeries(String channelId, Function<AwattarPrice, Double> valueFunction) {
|
private void createAndSendTimeSeries(String channelId, Function<AwattarPrice, Double> valueFunction) {
|
||||||
SortedSet<AwattarPrice> prices = getPrices();
|
SortedSet<AwattarPrice> locPrices = getPrices();
|
||||||
Unit<?> priceUnit = getPriceUnit();
|
Unit<?> priceUnit = getPriceUnit();
|
||||||
if (prices == null) {
|
if (locPrices == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
TimeSeries timeSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
|
TimeSeries timeSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
|
||||||
prices.forEach(p -> {
|
locPrices.forEach(p -> {
|
||||||
timeSeries.add(Instant.ofEpochMilli(p.timerange().start()),
|
timeSeries.add(Instant.ofEpochMilli(p.timerange().start()),
|
||||||
new QuantityType<>(valueFunction.apply(p) / 100.0, priceUnit));
|
new QuantityType<>(valueFunction.apply(p) / 100.0, priceUnit));
|
||||||
});
|
});
|
||||||
@ -232,9 +174,12 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
|
|||||||
* The data is refreshed if:
|
* The data is refreshed if:
|
||||||
* - the thing is offline
|
* - the thing is offline
|
||||||
* - the local cache is empty
|
* - the local cache is empty
|
||||||
* - the current time is after 15:00 and the last refresh was more than an hour ago
|
* - the current time is after 15:00 and the last refresh was more than an hour
|
||||||
* - the current time is after 18:00 and the last refresh was more than an hour ago
|
* ago
|
||||||
* - the current time is after 21:00 and the last refresh was more than an hour ago
|
* - the current time is after 18:00 and the last refresh was more than an hour
|
||||||
|
* ago
|
||||||
|
* - the current time is after 21:00 and the last refresh was more than an hour
|
||||||
|
* ago
|
||||||
*
|
*
|
||||||
* @return true if the data needs to be refreshed
|
* @return true if the data needs to be refreshed
|
||||||
*/
|
*/
|
||||||
@ -249,10 +194,12 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: all this magic is made to avoid refreshing the data too often, since the API is rate-limited
|
// Note: all this magic is made to avoid refreshing the data too often, since
|
||||||
|
// the API is rate-limited
|
||||||
// to 100 requests per day.
|
// to 100 requests per day.
|
||||||
|
|
||||||
// do not refresh before 15:00, since the prices for the next day are available only after 14:00
|
// do not refresh before 15:00, since the prices for the next day are available
|
||||||
|
// only after 14:00
|
||||||
ZonedDateTime now = ZonedDateTime.now(zone);
|
ZonedDateTime now = ZonedDateTime.now(zone);
|
||||||
if (now.getHour() < 15) {
|
if (now.getHour() < 15) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -29,7 +29,7 @@ thing-type.config.awattar.bestprice.length.description = The number of hours the
|
|||||||
thing-type.config.awattar.bestprice.consecutive.label = Consecutive
|
thing-type.config.awattar.bestprice.consecutive.label = Consecutive
|
||||||
thing-type.config.awattar.bestprice.consecutive.description = Do the hours need to be consecutive?
|
thing-type.config.awattar.bestprice.consecutive.description = Do the hours need to be consecutive?
|
||||||
thing-type.config.awattar.bestprice.inverted.label = Inverted
|
thing-type.config.awattar.bestprice.inverted.label = Inverted
|
||||||
thing-type.config.awattar.bestprice.inverted.description = Invert the search for the highest price
|
thing-type.config.awattar.bestprice.inverted.description = Invert the search to the highest price.
|
||||||
|
|
||||||
# channel types
|
# channel types
|
||||||
channel-type.awattar.price.label = ct/kWh
|
channel-type.awattar.price.label = ct/kWh
|
||||||
@ -167,7 +167,7 @@ error.json=Invalid JSON response from aWATTar API
|
|||||||
error.interrupted=Communication interrupted
|
error.interrupted=Communication interrupted
|
||||||
error.execution=Execution error
|
error.execution=Execution error
|
||||||
error.timeout=Timeout retrieving prices from aWATTar API
|
error.timeout=Timeout retrieving prices from aWATTar API
|
||||||
error.invalid.data=No or invalid data received from aWATTar API
|
error.empty.data=No or invalid data received from aWATTar API
|
||||||
error.length.value=length needs to be > 0 and < duration.
|
error.length.value=length needs to be > 0 and < duration.
|
||||||
warn.awattar.statuscode=aWATTar server did not respond with status code 200
|
warn.awattar.statuscode=aWATTar server did not respond with status code 200
|
||||||
error.start.value=Invalid start value
|
error.start.value=Invalid start value
|
||||||
|
@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* 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.api;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
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.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.AwattarBridgeConfiguration;
|
||||||
|
import org.openhab.binding.awattar.internal.AwattarPrice;
|
||||||
|
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
|
||||||
|
import org.openhab.binding.awattar.internal.handler.AwattarBridgeHandler;
|
||||||
|
import org.openhab.binding.awattar.internal.handler.AwattarBridgeHandlerTest;
|
||||||
|
import org.openhab.core.i18n.TimeZoneProvider;
|
||||||
|
import org.openhab.core.test.java.JavaTest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link AwattarBridgeHandlerTest} contains tests for the
|
||||||
|
* {@link AwattarBridgeHandler}
|
||||||
|
*
|
||||||
|
* @author Jan N. Klug - Initial contribution
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||||
|
@NonNullByDefault
|
||||||
|
class AwattarApiTest extends JavaTest {
|
||||||
|
// API Mocks
|
||||||
|
private @Mock @NonNullByDefault({}) HttpClient httpClientMock;
|
||||||
|
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
|
||||||
|
private @Mock @NonNullByDefault({}) Request requestMock;
|
||||||
|
private @Mock @NonNullByDefault({}) ContentResponse contentResponseMock;
|
||||||
|
private @Mock @NonNullByDefault({}) AwattarBridgeConfiguration config;
|
||||||
|
|
||||||
|
// sut
|
||||||
|
private @NonNullByDefault({}) AwattarApi api;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() throws IOException, ExecutionException, InterruptedException, TimeoutException {
|
||||||
|
try (InputStream inputStream = AwattarApiTest.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"));
|
||||||
|
|
||||||
|
config.basePrice = 0.0;
|
||||||
|
config.vatPercent = 0.0;
|
||||||
|
config.country = "DE";
|
||||||
|
|
||||||
|
api = new AwattarApi(httpClientMock, ZoneId.of("GMT+2"), config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeUrl() throws AwattarApiException {
|
||||||
|
api.getData();
|
||||||
|
|
||||||
|
assertThat(httpClientMock.newRequest("https://api.awattar.de/v1/marketdata"), is(requestMock));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAtUrl() throws AwattarApiException {
|
||||||
|
config.country = "AT";
|
||||||
|
api = new AwattarApi(httpClientMock, ZoneId.of("GMT+2"), config);
|
||||||
|
|
||||||
|
api.getData();
|
||||||
|
|
||||||
|
assertThat(httpClientMock.newRequest("https://api.awattar.at/v1/marketdata"), is(requestMock));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidCountry() {
|
||||||
|
config.country = "CH";
|
||||||
|
|
||||||
|
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> new AwattarApi(httpClientMock, ZoneId.of("GMT+2"), config));
|
||||||
|
assertThat(thrown.getMessage(), is("Country code must be 'DE' or 'AT'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPricesRetrieval() throws AwattarApiException {
|
||||||
|
SortedSet<AwattarPrice> prices = api.getData();
|
||||||
|
|
||||||
|
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
|
||||||
|
void testPricesRetrievalEmptyResponse() {
|
||||||
|
when(contentResponseMock.getContentAsString()).thenReturn(null);
|
||||||
|
when(contentResponseMock.getStatus()).thenReturn(HttpStatus.OK_200);
|
||||||
|
|
||||||
|
AwattarApiException thrown = assertThrows(AwattarApiException.class, () -> api.getData());
|
||||||
|
assertThat(thrown.getMessage(), is("@text/error.empty.data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPricesReturnNot200() {
|
||||||
|
when(contentResponseMock.getStatus()).thenReturn(HttpStatus.BAD_REQUEST_400);
|
||||||
|
|
||||||
|
AwattarApiException thrown = assertThrows(AwattarApiException.class, () -> api.getData());
|
||||||
|
assertThat(thrown.getMessage(), is("@text/warn.awattar.statuscode400"));
|
||||||
|
}
|
||||||
|
}
|
@ -12,31 +12,30 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.awattar.internal.handler;
|
package org.openhab.binding.awattar.internal.handler;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.*;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.lang.reflect.Field;
|
||||||
import java.io.InputStream;
|
import java.lang.reflect.Method;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.platform.commons.support.HierarchyTraversalMode;
|
||||||
|
import org.junit.platform.commons.support.ReflectionSupport;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.mockito.junit.jupiter.MockitoSettings;
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
import org.mockito.quality.Strictness;
|
import org.mockito.quality.Strictness;
|
||||||
import org.openhab.binding.awattar.internal.AwattarBindingConstants;
|
import org.openhab.binding.awattar.internal.AwattarBindingConstants;
|
||||||
|
import org.openhab.binding.awattar.internal.api.AwattarApi;
|
||||||
|
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
|
||||||
import org.openhab.core.i18n.TimeZoneProvider;
|
import org.openhab.core.i18n.TimeZoneProvider;
|
||||||
import org.openhab.core.test.java.JavaTest;
|
import org.openhab.core.test.java.JavaTest;
|
||||||
import org.openhab.core.thing.Bridge;
|
import org.openhab.core.thing.Bridge;
|
||||||
@ -48,14 +47,15 @@ import org.openhab.core.thing.ThingUID;
|
|||||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link AwattarBridgeHandlerRefreshTest} contains tests for the {@link AwattarBridgeHandler} refresh logic.
|
* The {@link AwattarBridgeHandlerRefreshTest} contains tests for the
|
||||||
|
* {@link AwattarBridgeHandler} refresh logic.
|
||||||
*
|
*
|
||||||
* @author Thomas Leber - Initial contribution
|
* @author Thomas Leber - Initial contribution
|
||||||
*/
|
*/
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class AwattarBridgeHandlerRefreshTest extends JavaTest {
|
class AwattarBridgeHandlerRefreshTest extends JavaTest {
|
||||||
public static final ThingUID BRIDGE_UID = new ThingUID(AwattarBindingConstants.THING_TYPE_BRIDGE, "testBridge");
|
public static final ThingUID BRIDGE_UID = new ThingUID(AwattarBindingConstants.THING_TYPE_BRIDGE, "testBridge");
|
||||||
|
|
||||||
// bridge mocks
|
// bridge mocks
|
||||||
@ -63,8 +63,7 @@ public class AwattarBridgeHandlerRefreshTest extends JavaTest {
|
|||||||
private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock;
|
private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock;
|
||||||
private @Mock @NonNullByDefault({}) HttpClient httpClientMock;
|
private @Mock @NonNullByDefault({}) HttpClient httpClientMock;
|
||||||
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
|
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
|
||||||
private @Mock @NonNullByDefault({}) Request requestMock;
|
private @Mock @NonNullByDefault({}) AwattarApi awattarApiMock;
|
||||||
private @Mock @NonNullByDefault({}) ContentResponse contentResponseMock;
|
|
||||||
|
|
||||||
// best price handler mocks
|
// best price handler mocks
|
||||||
private @Mock @NonNullByDefault({}) Thing bestpriceMock;
|
private @Mock @NonNullByDefault({}) Thing bestpriceMock;
|
||||||
@ -73,22 +72,7 @@ public class AwattarBridgeHandlerRefreshTest extends JavaTest {
|
|||||||
private @NonNullByDefault({}) AwattarBridgeHandler bridgeHandler;
|
private @NonNullByDefault({}) AwattarBridgeHandler bridgeHandler;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void setUp() throws IOException, ExecutionException, InterruptedException, TimeoutException {
|
public void setUp() throws IllegalArgumentException, IllegalAccessException {
|
||||||
try (InputStream inputStream = AwattarBridgeHandlerRefreshTest.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"));
|
when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of("GMT+2"));
|
||||||
|
|
||||||
@ -96,42 +80,79 @@ public class AwattarBridgeHandlerRefreshTest extends JavaTest {
|
|||||||
bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock);
|
bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock);
|
||||||
bridgeHandler.setCallback(bridgeCallbackMock);
|
bridgeHandler.setCallback(bridgeCallbackMock);
|
||||||
|
|
||||||
when(bridgeMock.getHandler()).thenReturn(bridgeHandler);
|
List<Field> fields = ReflectionSupport.findFields(AwattarBridgeHandler.class,
|
||||||
|
field -> field.getName().equals("awattarApi"), HierarchyTraversalMode.BOTTOM_UP);
|
||||||
|
|
||||||
// other mocks
|
for (Field field : fields) {
|
||||||
when(bestpriceMock.getBridgeUID()).thenReturn(BRIDGE_UID);
|
field.setAccessible(true);
|
||||||
|
field.set(bridgeHandler, awattarApiMock);
|
||||||
when(bestPriceCallbackMock.getBridge(any())).thenReturn(bridgeMock);
|
}
|
||||||
when(bestPriceCallbackMock.isChannelLinked(any())).thenReturn(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the refreshIfNeeded method with a bridge that is offline.
|
* Test the refreshIfNeeded method with a bridge that is offline.
|
||||||
*
|
*
|
||||||
* @throws SecurityException
|
* @throws SecurityException
|
||||||
|
* @throws AwattarApiException
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void testRefreshIfNeeded_ThingOffline() throws SecurityException {
|
void testRefreshIfNeeded_ThingOffline() throws SecurityException, AwattarApiException {
|
||||||
when(bridgeMock.getStatus()).thenReturn(ThingStatus.OFFLINE);
|
when(bridgeMock.getStatus()).thenReturn(ThingStatus.OFFLINE);
|
||||||
|
|
||||||
bridgeHandler.refreshIfNeeded();
|
bridgeHandler.refreshIfNeeded();
|
||||||
|
|
||||||
verify(bridgeCallbackMock).statusUpdated(bridgeMock,
|
verify(bridgeCallbackMock).statusUpdated(bridgeMock,
|
||||||
new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
|
new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
|
||||||
|
verify(awattarApiMock).getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the refreshIfNeeded method with a bridge that is online and the data is empty.
|
* Test the refreshIfNeeded method with a bridge that is online and the data is
|
||||||
|
* empty.
|
||||||
*
|
*
|
||||||
* @throws SecurityException
|
* @throws SecurityException
|
||||||
|
* @throws AwattarApiException
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void testRefreshIfNeeded_DataEmptry() throws SecurityException {
|
void testRefreshIfNeeded_DataEmpty() throws SecurityException, AwattarApiException {
|
||||||
when(bridgeMock.getStatus()).thenReturn(ThingStatus.ONLINE);
|
when(bridgeMock.getStatus()).thenReturn(ThingStatus.ONLINE);
|
||||||
|
|
||||||
bridgeHandler.refreshIfNeeded();
|
bridgeHandler.refreshIfNeeded();
|
||||||
|
|
||||||
verify(bridgeCallbackMock).statusUpdated(bridgeMock,
|
verify(bridgeCallbackMock).statusUpdated(bridgeMock,
|
||||||
new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
|
new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
|
||||||
|
verify(awattarApiMock).getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNeedRefresh_ThingOffline() throws SecurityException {
|
||||||
|
when(bridgeMock.getStatus()).thenReturn(ThingStatus.OFFLINE);
|
||||||
|
|
||||||
|
// get private method via reflection
|
||||||
|
Method method = ReflectionSupport.findMethod(AwattarBridgeHandler.class, "needRefresh", "").get();
|
||||||
|
|
||||||
|
boolean result = (boolean) ReflectionSupport.invokeMethod(method, bridgeHandler);
|
||||||
|
|
||||||
|
assertThat(result, is(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNeedRefresh_DataEmpty() throws SecurityException, IllegalArgumentException, IllegalAccessException {
|
||||||
|
when(bridgeMock.getStatus()).thenReturn(ThingStatus.ONLINE);
|
||||||
|
|
||||||
|
List<Field> fields = ReflectionSupport.findFields(AwattarBridgeHandler.class,
|
||||||
|
field -> field.getName().equals("prices"), HierarchyTraversalMode.BOTTOM_UP);
|
||||||
|
|
||||||
|
for (Field field : fields) {
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(bridgeHandler, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get private method via reflection
|
||||||
|
Method method = ReflectionSupport.findMethod(AwattarBridgeHandler.class, "needRefresh", "").get();
|
||||||
|
|
||||||
|
boolean result = (boolean) ReflectionSupport.invokeMethod(method, bridgeHandler);
|
||||||
|
|
||||||
|
assertThat(result, is(true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,41 +13,48 @@
|
|||||||
package org.openhab.binding.awattar.internal.handler;
|
package org.openhab.binding.awattar.internal.handler;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.closeTo;
|
||||||
import static org.mockito.ArgumentMatchers.*;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.*;
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_END;
|
||||||
|
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_HOURS;
|
||||||
|
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_START;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.lang.reflect.Field;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.SortedSet;
|
import java.util.SortedSet;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.junit.platform.commons.support.HierarchyTraversalMode;
|
||||||
|
import org.junit.platform.commons.support.ReflectionSupport;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.mockito.junit.jupiter.MockitoSettings;
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
import org.mockito.quality.Strictness;
|
import org.mockito.quality.Strictness;
|
||||||
import org.openhab.binding.awattar.internal.AwattarBindingConstants;
|
import org.openhab.binding.awattar.internal.AwattarBindingConstants;
|
||||||
import org.openhab.binding.awattar.internal.AwattarPrice;
|
import org.openhab.binding.awattar.internal.AwattarPrice;
|
||||||
|
import org.openhab.binding.awattar.internal.api.AwattarApi;
|
||||||
|
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
|
||||||
|
import org.openhab.binding.awattar.internal.dto.AwattarApiData;
|
||||||
import org.openhab.core.config.core.Configuration;
|
import org.openhab.core.config.core.Configuration;
|
||||||
import org.openhab.core.i18n.TimeZoneProvider;
|
import org.openhab.core.i18n.TimeZoneProvider;
|
||||||
import org.openhab.core.library.types.DateTimeType;
|
import org.openhab.core.library.types.DateTimeType;
|
||||||
@ -60,6 +67,8 @@ import org.openhab.core.thing.ThingUID;
|
|||||||
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||||
import org.openhab.core.types.State;
|
import org.openhab.core.types.State;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link AwattarBridgeHandlerTest} contains tests for the {@link AwattarBridgeHandler}
|
* The {@link AwattarBridgeHandlerTest} contains tests for the {@link AwattarBridgeHandler}
|
||||||
*
|
*
|
||||||
@ -76,8 +85,7 @@ public class AwattarBridgeHandlerTest extends JavaTest {
|
|||||||
private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock;
|
private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock;
|
||||||
private @Mock @NonNullByDefault({}) HttpClient httpClientMock;
|
private @Mock @NonNullByDefault({}) HttpClient httpClientMock;
|
||||||
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
|
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
|
||||||
private @Mock @NonNullByDefault({}) Request requestMock;
|
private @Mock @NonNullByDefault({}) AwattarApi awattarApiMock;
|
||||||
private @Mock @NonNullByDefault({}) ContentResponse contentResponseMock;
|
|
||||||
|
|
||||||
// best price handler mocks
|
// best price handler mocks
|
||||||
private @Mock @NonNullByDefault({}) Thing bestpriceMock;
|
private @Mock @NonNullByDefault({}) Thing bestpriceMock;
|
||||||
@ -86,69 +94,64 @@ public class AwattarBridgeHandlerTest extends JavaTest {
|
|||||||
private @NonNullByDefault({}) AwattarBridgeHandler bridgeHandler;
|
private @NonNullByDefault({}) AwattarBridgeHandler bridgeHandler;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void setUp() throws IOException, ExecutionException, InterruptedException, TimeoutException {
|
public void setUp() throws IOException, IllegalArgumentException, IllegalAccessException, AwattarApiException {
|
||||||
|
|
||||||
|
// mock the API response
|
||||||
try (InputStream inputStream = AwattarBridgeHandlerTest.class.getResourceAsStream("api_response.json")) {
|
try (InputStream inputStream = AwattarBridgeHandlerTest.class.getResourceAsStream("api_response.json")) {
|
||||||
if (inputStream == null) {
|
SortedSet<AwattarPrice> result = new TreeSet<>(Comparator.comparing(AwattarPrice::timerange));
|
||||||
throw new IOException("inputstream is null");
|
Gson gson = new Gson();
|
||||||
|
|
||||||
|
String json = new String(inputStream.readAllBytes());
|
||||||
|
|
||||||
|
// read json file into sorted set of AwattarPrices
|
||||||
|
AwattarApiData apiData = gson.fromJson(json, AwattarApiData.class);
|
||||||
|
apiData.data.forEach(datum -> result.add(new AwattarPrice(datum.marketprice, datum.marketprice,
|
||||||
|
datum.marketprice, datum.marketprice, new TimeRange(datum.startTimestamp, datum.endTimestamp))));
|
||||||
|
when(awattarApiMock.getData()).thenReturn(result);
|
||||||
}
|
}
|
||||||
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"));
|
when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of("GMT+2"));
|
||||||
|
|
||||||
when(bridgeMock.getUID()).thenReturn(BRIDGE_UID);
|
when(bridgeMock.getUID()).thenReturn(BRIDGE_UID);
|
||||||
bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock);
|
bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock);
|
||||||
bridgeHandler.setCallback(bridgeCallbackMock);
|
bridgeHandler.setCallback(bridgeCallbackMock);
|
||||||
|
|
||||||
|
// mock the private field awattarApi
|
||||||
|
List<Field> fields = ReflectionSupport.findFields(AwattarBridgeHandler.class,
|
||||||
|
field -> field.getName().equals("awattarApi"), HierarchyTraversalMode.BOTTOM_UP);
|
||||||
|
|
||||||
|
for (Field field : fields) {
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(bridgeHandler, awattarApiMock);
|
||||||
|
}
|
||||||
|
|
||||||
bridgeHandler.refreshIfNeeded();
|
bridgeHandler.refreshIfNeeded();
|
||||||
when(bridgeMock.getHandler()).thenReturn(bridgeHandler);
|
when(bridgeMock.getHandler()).thenReturn(bridgeHandler);
|
||||||
|
|
||||||
// other mocks
|
// other mocks
|
||||||
when(bestpriceMock.getBridgeUID()).thenReturn(BRIDGE_UID);
|
when(bestpriceMock.getBridgeUID()).thenReturn(BRIDGE_UID);
|
||||||
|
|
||||||
when(bestPriceCallbackMock.getBridge(any())).thenReturn(bridgeMock);
|
when(bestPriceCallbackMock.getBridge(any())).thenReturn(bridgeMock);
|
||||||
when(bestPriceCallbackMock.isChannelLinked(any())).thenReturn(true);
|
when(bestPriceCallbackMock.isChannelLinked(any())).thenReturn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPricesRetrieval() {
|
void testGetPriceForSuccess() {
|
||||||
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);
|
AwattarPrice price = bridgeHandler.getPriceFor(1718503200000L);
|
||||||
|
|
||||||
assertThat(price, is(notNullValue()));
|
assertThat(price, is(notNullValue()));
|
||||||
Objects.requireNonNull(price);
|
Objects.requireNonNull(price);
|
||||||
assertThat(price.netPrice(), is(closeTo(0.219, 0.001)));
|
assertThat(price.netPrice(), is(closeTo(2.19, 0.001)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetPriceForFail() {
|
void testGetPriceForFail() {
|
||||||
AwattarPrice price = bridgeHandler.getPriceFor(1518503200000L);
|
AwattarPrice price = bridgeHandler.getPriceFor(1518503200000L);
|
||||||
|
|
||||||
assertThat(price, is(nullValue()));
|
assertThat(price, is(nullValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testContainsPrizeFor() {
|
void testContainsPrizeFor() {
|
||||||
assertThat(bridgeHandler.containsPriceFor(1618503200000L), is(false));
|
assertThat(bridgeHandler.containsPriceFor(1618503200000L), is(false));
|
||||||
assertThat(bridgeHandler.containsPriceFor(1718503200000L), is(true));
|
assertThat(bridgeHandler.containsPriceFor(1718503200000L), is(true));
|
||||||
assertThat(bridgeHandler.containsPriceFor(1818503200000L), is(false));
|
assertThat(bridgeHandler.containsPriceFor(1818503200000L), is(false));
|
||||||
@ -172,7 +175,7 @@ public class AwattarBridgeHandlerTest extends JavaTest {
|
|||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource
|
@MethodSource
|
||||||
public void testBestpriceHandler(int length, boolean consecutive, String channelId, State expectedState) {
|
void testBestpriceHandler(int length, boolean consecutive, String channelId, State expectedState) {
|
||||||
ThingUID bestPriceUid = new ThingUID(AwattarBindingConstants.THING_TYPE_BESTPRICE, "foo");
|
ThingUID bestPriceUid = new ThingUID(AwattarBindingConstants.THING_TYPE_BESTPRICE, "foo");
|
||||||
Map<String, Object> config = Map.of("length", length, "consecutive", consecutive);
|
Map<String, Object> config = Map.of("length", length, "consecutive", consecutive);
|
||||||
when(bestpriceMock.getConfiguration()).thenReturn(new Configuration(config));
|
when(bestpriceMock.getConfiguration()).thenReturn(new Configuration(config));
|
||||||
|
@ -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