*
* @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,
--- /dev/null
+/**
+ * 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");
+ }
+ }
+}
* here
*/
thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
- getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000,
+ getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000L,
TimeUnit.MILLISECONDS);
}
}
*/
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;
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;
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" />
*
*
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();
}
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
}
}
+ /**
+ * 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();
-
- 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 netTotalSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
-
- 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");
+ // Method is private and only called when dataRefresher is initialized.
+ // DataRefresher is initialized after successful creation of AwattarApi.
+ prices = awattarApi.getData();
+
+ TimeSeries netMarketSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
+ TimeSeries netTotalSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
+
+ Unit<?> priceUnit = getPriceUnit();
+
+ for (AwattarPrice price : prices) {
+ Instant timestamp = Instant.ofEpochMilli(price.timerange().start());
+
+ 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());
}
}
}
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));
});
* 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
*/
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;
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
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
--- /dev/null
+/**
+ * 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"));
+ }
+}
*/
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;
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
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;
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"));
bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock);
bridgeHandler.setCallback(bridgeCallbackMock);
- when(bridgeMock.getHandler()).thenReturn(bridgeHandler);
-
- // other mocks
- when(bestpriceMock.getBridgeUID()).thenReturn(BRIDGE_UID);
+ List<Field> fields = ReflectionSupport.findFields(AwattarBridgeHandler.class,
+ field -> field.getName().equals("awattarApi"), HierarchyTraversalMode.BOTTOM_UP);
- 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));
}
}
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;
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}
*
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;
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));
@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));
--- /dev/null
+{
+ "object": "list",
+ "data": [
+ {
+ "start_timestamp": 1718316000000,
+ "end_timestamp": 1718319600000,
+ "marketprice": 83.13,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718319600000,
+ "end_timestamp": 1718323200000,
+ "marketprice": 71.45,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718323200000,
+ "end_timestamp": 1718326800000,
+ "marketprice": 63.93,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718326800000,
+ "end_timestamp": 1718330400000,
+ "marketprice": 59.53,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718330400000,
+ "end_timestamp": 1718334000000,
+ "marketprice": 55.82,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718334000000,
+ "end_timestamp": 1718337600000,
+ "marketprice": 64.22,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718337600000,
+ "end_timestamp": 1718341200000,
+ "marketprice": 85.01,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718341200000,
+ "end_timestamp": 1718344800000,
+ "marketprice": 100.95,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718344800000,
+ "end_timestamp": 1718348400000,
+ "marketprice": 104.99,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718348400000,
+ "end_timestamp": 1718352000000,
+ "marketprice": 102.54,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718352000000,
+ "end_timestamp": 1718355600000,
+ "marketprice": 82.18,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718355600000,
+ "end_timestamp": 1718359200000,
+ "marketprice": 68.1,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718359200000,
+ "end_timestamp": 1718362800000,
+ "marketprice": 60.88,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718362800000,
+ "end_timestamp": 1718366400000,
+ "marketprice": 47.46,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718366400000,
+ "end_timestamp": 1718370000000,
+ "marketprice": 40.74,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718370000000,
+ "end_timestamp": 1718373600000,
+ "marketprice": 41,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718373600000,
+ "end_timestamp": 1718377200000,
+ "marketprice": 60.31,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718377200000,
+ "end_timestamp": 1718380800000,
+ "marketprice": 75,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718380800000,
+ "end_timestamp": 1718384400000,
+ "marketprice": 90.98,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718384400000,
+ "end_timestamp": 1718388000000,
+ "marketprice": 136,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718388000000,
+ "end_timestamp": 1718391600000,
+ "marketprice": 127.31,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718391600000,
+ "end_timestamp": 1718395200000,
+ "marketprice": 117.12,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718395200000,
+ "end_timestamp": 1718398800000,
+ "marketprice": 83.41,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718398800000,
+ "end_timestamp": 1718402400000,
+ "marketprice": 59.42,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718402400000,
+ "end_timestamp": 1718406000000,
+ "marketprice": 60.68,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718406000000,
+ "end_timestamp": 1718409600000,
+ "marketprice": 41.04,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718409600000,
+ "end_timestamp": 1718413200000,
+ "marketprice": 29.97,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718413200000,
+ "end_timestamp": 1718416800000,
+ "marketprice": 28.86,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718416800000,
+ "end_timestamp": 1718420400000,
+ "marketprice": 22.51,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718420400000,
+ "end_timestamp": 1718424000000,
+ "marketprice": 10.04,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718424000000,
+ "end_timestamp": 1718427600000,
+ "marketprice": 1.54,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718427600000,
+ "end_timestamp": 1718431200000,
+ "marketprice": 0.09,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718431200000,
+ "end_timestamp": 1718434800000,
+ "marketprice": 0,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718434800000,
+ "end_timestamp": 1718438400000,
+ "marketprice": -0.06,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718438400000,
+ "end_timestamp": 1718442000000,
+ "marketprice": -10.08,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718442000000,
+ "end_timestamp": 1718445600000,
+ "marketprice": -29.04,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718445600000,
+ "end_timestamp": 1718449200000,
+ "marketprice": -44.92,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718449200000,
+ "end_timestamp": 1718452800000,
+ "marketprice": -65.46,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718452800000,
+ "end_timestamp": 1718456400000,
+ "marketprice": -80.01,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718456400000,
+ "end_timestamp": 1718460000000,
+ "marketprice": -56.23,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718460000000,
+ "end_timestamp": 1718463600000,
+ "marketprice": -29.53,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718463600000,
+ "end_timestamp": 1718467200000,
+ "marketprice": -4.84,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718467200000,
+ "end_timestamp": 1718470800000,
+ "marketprice": -0.01,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718470800000,
+ "end_timestamp": 1718474400000,
+ "marketprice": 40,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718474400000,
+ "end_timestamp": 1718478000000,
+ "marketprice": 84.28,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718478000000,
+ "end_timestamp": 1718481600000,
+ "marketprice": 79.92,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718481600000,
+ "end_timestamp": 1718485200000,
+ "marketprice": 64.3,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718485200000,
+ "end_timestamp": 1718488800000,
+ "marketprice": 40.4,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718488800000,
+ "end_timestamp": 1718492400000,
+ "marketprice": 24.91,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718492400000,
+ "end_timestamp": 1718496000000,
+ "marketprice": 10.36,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718496000000,
+ "end_timestamp": 1718499600000,
+ "marketprice": 4.92,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718499600000,
+ "end_timestamp": 1718503200000,
+ "marketprice": 2.92,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718503200000,
+ "end_timestamp": 1718506800000,
+ "marketprice": 2.19,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718506800000,
+ "end_timestamp": 1718510400000,
+ "marketprice": 2.53,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718510400000,
+ "end_timestamp": 1718514000000,
+ "marketprice": 2.95,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718514000000,
+ "end_timestamp": 1718517600000,
+ "marketprice": 0.69,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718517600000,
+ "end_timestamp": 1718521200000,
+ "marketprice": -0.02,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718521200000,
+ "end_timestamp": 1718524800000,
+ "marketprice": -1.28,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718524800000,
+ "end_timestamp": 1718528400000,
+ "marketprice": -10,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718528400000,
+ "end_timestamp": 1718532000000,
+ "marketprice": -13.33,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718532000000,
+ "end_timestamp": 1718535600000,
+ "marketprice": -20.01,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718535600000,
+ "end_timestamp": 1718539200000,
+ "marketprice": -30.01,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718539200000,
+ "end_timestamp": 1718542800000,
+ "marketprice": -35.67,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718542800000,
+ "end_timestamp": 1718546400000,
+ "marketprice": -29.04,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718546400000,
+ "end_timestamp": 1718550000000,
+ "marketprice": -10.14,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718550000000,
+ "end_timestamp": 1718553600000,
+ "marketprice": -2.34,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718553600000,
+ "end_timestamp": 1718557200000,
+ "marketprice": 56.22,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718557200000,
+ "end_timestamp": 1718560800000,
+ "marketprice": 99.65,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718560800000,
+ "end_timestamp": 1718564400000,
+ "marketprice": 119.15,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718564400000,
+ "end_timestamp": 1718568000000,
+ "marketprice": 124.28,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718568000000,
+ "end_timestamp": 1718571600000,
+ "marketprice": 120.34,
+ "unit": "Eur/MWh"
+ },
+ {
+ "start_timestamp": 1718571600000,
+ "end_timestamp": 1718575200000,
+ "marketprice": 94.44,
+ "unit": "Eur/MWh"
+ }
+ ],
+ "url": "/de/v1/marketdata"
+}
\ No newline at end of file