mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +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 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
|
||||
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
|
||||
*/
|
||||
thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
|
||||
getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000,
|
||||
getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000L,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
@ -12,21 +12,15 @@
|
||||
*/
|
||||
package org.openhab.binding.awattar.internal.handler;
|
||||
|
||||
import static org.eclipse.jetty.http.HttpMethod.GET;
|
||||
import static org.eclipse.jetty.http.HttpStatus.OK_200;
|
||||
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.*;
|
||||
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_MARKET_NET;
|
||||
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_TOTAL_NET;
|
||||
|
||||
import java.time.Instant;
|
||||
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.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.measure.Unit;
|
||||
@ -34,11 +28,10 @@ import javax.measure.Unit;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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.api.AwattarApi;
|
||||
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
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.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" />
|
||||
*
|
||||
*
|
||||
@ -73,25 +65,19 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AwattarBridgeHandler.class);
|
||||
private final HttpClient httpClient;
|
||||
|
||||
private @Nullable ScheduledFuture<?> dataRefresher;
|
||||
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
|
||||
private @Nullable SortedSet<AwattarPrice> prices;
|
||||
private double vatFactor = 0;
|
||||
private double basePrice = 0;
|
||||
private ZoneId zone;
|
||||
private final TimeZoneProvider timeZoneProvider;
|
||||
|
||||
private @Nullable AwattarApi awattarApi;
|
||||
|
||||
public AwattarBridgeHandler(Bridge thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
|
||||
super(thing);
|
||||
this.httpClient = httpClient;
|
||||
url = URLDE;
|
||||
this.timeZoneProvider = timeZoneProvider;
|
||||
zone = timeZoneProvider.getTimeZone();
|
||||
}
|
||||
|
||||
@ -99,24 +85,15 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
|
||||
public void initialize() {
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
AwattarBridgeConfiguration config = getConfigAs(AwattarBridgeConfiguration.class);
|
||||
vatFactor = 1 + (config.vatPercent / 100);
|
||||
basePrice = config.basePrice;
|
||||
zone = timeZoneProvider.getTimeZone();
|
||||
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,
|
||||
TimeUnit.MILLISECONDS);
|
||||
try {
|
||||
awattarApi = new AwattarApi(httpClient, zone, config);
|
||||
|
||||
dataRefresher = scheduler.scheduleWithFixedDelay(this::refreshIfNeeded, 0, DATA_REFRESH_INTERVAL * 1000L,
|
||||
TimeUnit.MILLISECONDS);
|
||||
} catch (IllegalArgumentException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.unsupported.country");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -135,71 +112,36 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data from the API.
|
||||
*
|
||||
*
|
||||
*/
|
||||
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);
|
||||
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();
|
||||
// Method is private and only called when dataRefresher is initialized.
|
||||
// DataRefresher is initialized after successful creation of AwattarApi.
|
||||
prices = awattarApi.getData();
|
||||
|
||||
StringBuilder request = new StringBuilder(url);
|
||||
request.append("?start=").append(start).append("&end=").append(end);
|
||||
TimeSeries netMarketSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
|
||||
TimeSeries netTotalSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
|
||||
|
||||
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);
|
||||
Unit<?> priceUnit = getPriceUnit();
|
||||
|
||||
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 netTotalSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
|
||||
for (AwattarPrice price : prices) {
|
||||
Instant timestamp = Instant.ofEpochMilli(price.timerange().start());
|
||||
|
||||
Unit<?> priceUnit = getPriceUnit();
|
||||
|
||||
for (Datum d : apiData.data) {
|
||||
double netMarket = d.marketprice / 10.0;
|
||||
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));
|
||||
netTotalSeries.add(timestamp, new QuantityType<>(netTotal / 100.0, priceUnit));
|
||||
|
||||
result.add(new AwattarPrice(netMarket, grossMarket, netTotal, grossTotal,
|
||||
new TimeRange(d.startTimestamp, d.endTimestamp)));
|
||||
}
|
||||
prices = result;
|
||||
|
||||
// update channels
|
||||
sendTimeSeries(CHANNEL_MARKET_NET, netMarketSeries);
|
||||
sendTimeSeries(CHANNEL_TOTAL_NET, netTotalSeries);
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/error.invalid.data");
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/warn.awattar.statuscode");
|
||||
netMarketSeries.add(timestamp, new QuantityType<>(price.netPrice() / 100.0, priceUnit));
|
||||
netTotalSeries.add(timestamp, new QuantityType<>(price.netTotal() / 100.0, priceUnit));
|
||||
}
|
||||
} 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");
|
||||
|
||||
// update channels
|
||||
sendTimeSeries(CHANNEL_MARKET_NET, netMarketSeries);
|
||||
sendTimeSeries(CHANNEL_TOTAL_NET, netTotalSeries);
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (AwattarApiException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,13 +155,13 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
|
||||
}
|
||||
|
||||
private void createAndSendTimeSeries(String channelId, Function<AwattarPrice, Double> valueFunction) {
|
||||
SortedSet<AwattarPrice> prices = getPrices();
|
||||
SortedSet<AwattarPrice> locPrices = getPrices();
|
||||
Unit<?> priceUnit = getPriceUnit();
|
||||
if (prices == null) {
|
||||
if (locPrices == null) {
|
||||
return;
|
||||
}
|
||||
TimeSeries timeSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
|
||||
prices.forEach(p -> {
|
||||
locPrices.forEach(p -> {
|
||||
timeSeries.add(Instant.ofEpochMilli(p.timerange().start()),
|
||||
new QuantityType<>(valueFunction.apply(p) / 100.0, priceUnit));
|
||||
});
|
||||
@ -232,9 +174,12 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
|
||||
* The data is refreshed if:
|
||||
* - the thing is offline
|
||||
* - 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 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
|
||||
* - the current time is after 15: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
|
||||
*/
|
||||
@ -249,10 +194,12 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
|
||||
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.
|
||||
|
||||
// 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);
|
||||
if (now.getHour() < 15) {
|
||||
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.description = Do the hours need to be consecutive?
|
||||
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-type.awattar.price.label = ct/kWh
|
||||
@ -167,7 +167,7 @@ error.json=Invalid JSON response from aWATTar API
|
||||
error.interrupted=Communication interrupted
|
||||
error.execution=Execution error
|
||||
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.
|
||||
warn.awattar.statuscode=aWATTar server did not respond with status code 200
|
||||
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;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.ZoneId;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.List;
|
||||
|
||||
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.platform.commons.support.HierarchyTraversalMode;
|
||||
import org.junit.platform.commons.support.ReflectionSupport;
|
||||
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.api.AwattarApi;
|
||||
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
|
||||
import org.openhab.core.i18n.TimeZoneProvider;
|
||||
import org.openhab.core.test.java.JavaTest;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
@ -48,14 +47,15 @@ import org.openhab.core.thing.ThingUID;
|
||||
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
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@NonNullByDefault
|
||||
public class AwattarBridgeHandlerRefreshTest extends JavaTest {
|
||||
class AwattarBridgeHandlerRefreshTest extends JavaTest {
|
||||
public static final ThingUID BRIDGE_UID = new ThingUID(AwattarBindingConstants.THING_TYPE_BRIDGE, "testBridge");
|
||||
|
||||
// bridge mocks
|
||||
@ -63,8 +63,7 @@ public class AwattarBridgeHandlerRefreshTest extends JavaTest {
|
||||
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;
|
||||
private @Mock @NonNullByDefault({}) AwattarApi awattarApiMock;
|
||||
|
||||
// best price handler mocks
|
||||
private @Mock @NonNullByDefault({}) Thing bestpriceMock;
|
||||
@ -73,22 +72,7 @@ public class AwattarBridgeHandlerRefreshTest extends JavaTest {
|
||||
private @NonNullByDefault({}) AwattarBridgeHandler bridgeHandler;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws IOException, ExecutionException, InterruptedException, TimeoutException {
|
||||
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);
|
||||
public void setUp() throws IllegalArgumentException, IllegalAccessException {
|
||||
|
||||
when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of("GMT+2"));
|
||||
|
||||
@ -96,42 +80,79 @@ public class AwattarBridgeHandlerRefreshTest extends JavaTest {
|
||||
bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock);
|
||||
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
|
||||
when(bestpriceMock.getBridgeUID()).thenReturn(BRIDGE_UID);
|
||||
|
||||
when(bestPriceCallbackMock.getBridge(any())).thenReturn(bridgeMock);
|
||||
when(bestPriceCallbackMock.isChannelLinked(any())).thenReturn(true);
|
||||
for (Field field : fields) {
|
||||
field.setAccessible(true);
|
||||
field.set(bridgeHandler, awattarApiMock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the refreshIfNeeded method with a bridge that is offline.
|
||||
*
|
||||
* @throws SecurityException
|
||||
* @throws AwattarApiException
|
||||
*/
|
||||
@Test
|
||||
void testRefreshIfNeeded_ThingOffline() throws SecurityException {
|
||||
void testRefreshIfNeeded_ThingOffline() throws SecurityException, AwattarApiException {
|
||||
when(bridgeMock.getStatus()).thenReturn(ThingStatus.OFFLINE);
|
||||
|
||||
bridgeHandler.refreshIfNeeded();
|
||||
|
||||
verify(bridgeCallbackMock).statusUpdated(bridgeMock,
|
||||
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 AwattarApiException
|
||||
*/
|
||||
@Test
|
||||
void testRefreshIfNeeded_DataEmptry() throws SecurityException {
|
||||
void testRefreshIfNeeded_DataEmpty() throws SecurityException, AwattarApiException {
|
||||
when(bridgeMock.getStatus()).thenReturn(ThingStatus.ONLINE);
|
||||
|
||||
bridgeHandler.refreshIfNeeded();
|
||||
|
||||
verify(bridgeCallbackMock).statusUpdated(bridgeMock,
|
||||
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;
|
||||
|
||||
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 static org.hamcrest.Matchers.closeTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
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.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
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.TreeSet;
|
||||
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.junit.platform.commons.support.HierarchyTraversalMode;
|
||||
import org.junit.platform.commons.support.ReflectionSupport;
|
||||
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.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.i18n.TimeZoneProvider;
|
||||
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.types.State;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
/**
|
||||
* 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({}) HttpClient httpClientMock;
|
||||
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
|
||||
private @Mock @NonNullByDefault({}) Request requestMock;
|
||||
private @Mock @NonNullByDefault({}) ContentResponse contentResponseMock;
|
||||
private @Mock @NonNullByDefault({}) AwattarApi awattarApiMock;
|
||||
|
||||
// best price handler mocks
|
||||
private @Mock @NonNullByDefault({}) Thing bestpriceMock;
|
||||
@ -86,69 +94,64 @@ public class AwattarBridgeHandlerTest extends JavaTest {
|
||||
private @NonNullByDefault({}) AwattarBridgeHandler bridgeHandler;
|
||||
|
||||
@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")) {
|
||||
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));
|
||||
SortedSet<AwattarPrice> result = new TreeSet<>(Comparator.comparing(AwattarPrice::timerange));
|
||||
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);
|
||||
}
|
||||
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(bridgeMock.getUID()).thenReturn(BRIDGE_UID);
|
||||
bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock);
|
||||
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();
|
||||
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() {
|
||||
void testGetPriceForSuccess() {
|
||||
AwattarPrice price = bridgeHandler.getPriceFor(1718503200000L);
|
||||
|
||||
assertThat(price, is(notNullValue()));
|
||||
Objects.requireNonNull(price);
|
||||
assertThat(price.netPrice(), is(closeTo(0.219, 0.001)));
|
||||
assertThat(price.netPrice(), is(closeTo(2.19, 0.001)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPriceForFail() {
|
||||
void testGetPriceForFail() {
|
||||
AwattarPrice price = bridgeHandler.getPriceFor(1518503200000L);
|
||||
|
||||
assertThat(price, is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContainsPrizeFor() {
|
||||
void testContainsPrizeFor() {
|
||||
assertThat(bridgeHandler.containsPriceFor(1618503200000L), is(false));
|
||||
assertThat(bridgeHandler.containsPriceFor(1718503200000L), is(true));
|
||||
assertThat(bridgeHandler.containsPriceFor(1818503200000L), is(false));
|
||||
@ -172,7 +175,7 @@ public class AwattarBridgeHandlerTest extends JavaTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@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");
|
||||
Map<String, Object> config = Map.of("length", length, "consecutive", consecutive);
|
||||
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