*/
@NonNullByDefault
public abstract class AwattarBestPriceResult {
-
private long start;
private long end;
*/
@NonNullByDefault
public class AwattarBestpriceConfiguration {
-
- public int rangeStart;
- public int rangeDuration;
- public int length;
- public boolean consecutive;
+ public int rangeStart = 0;
+ public int rangeDuration = 24;
+ public int length = 1;
+ public boolean consecutive = true;
@Override
public String toString() {
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
-import org.openhab.core.thing.type.ChannelGroupTypeUID;
/**
* The {@link AwattarBindingConstants} class defines common constants, which are
*/
@NonNullByDefault
public class AwattarBindingConstants {
-
public static final String BINDING_ID = "awattar";
- public static final String API = "api";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
public static final ThingTypeUID THING_TYPE_PRICE = new ThingTypeUID(BINDING_ID, "prices");
public static final ThingTypeUID THING_TYPE_BESTPRICE = new ThingTypeUID(BINDING_ID, "bestprice");
- public static final ThingTypeUID THING_TYPE_BESTNEXT = new ThingTypeUID(BINDING_ID, "bestnext");
-
- public static final ChannelGroupTypeUID CHANNEL_GROUP_TYPE_HOURLY_PRICES = new ChannelGroupTypeUID(BINDING_ID,
- "hourly-prices");
public static final String CHANNEL_GROUP_CURRENT = "current";
public static final String CHANNEL_COUNTDOWN = "countdown";
public static final String CHANNEL_REMAINING = "remaining";
public static final String CHANNEL_HOURS = "hours";
- public static final String CHANNEL_DURATION = "rangeDuration";
- public static final String CHANNEL_LOOKUP_HOURS = "lookupHours";
- public static final String CHANNEL_CONSECUTIVE = "consecutive";
}
*/
@NonNullByDefault
public class AwattarBridgeConfiguration {
-
public double basePrice;
public double vatPercent;
public String country = "";
*/
@NonNullByDefault
public class AwattarConsecutiveBestPriceResult extends AwattarBestPriceResult {
-
private double priceSum = 0;
private int length = 0;
- private String hours;
- private ZoneId zoneId;
+ private final String hours;
+ private final ZoneId zoneId;
public AwattarConsecutiveBestPriceResult(List<AwattarPrice> prices, ZoneId zoneId) {
super();
StringBuilder hours = new StringBuilder();
boolean second = false;
for (AwattarPrice price : prices) {
- priceSum += price.getPrice();
+ priceSum += price.netPrice();
length++;
- updateStart(price.getStartTimestamp());
- updateEnd(price.getEndTimestamp());
+ updateStart(price.timerange().start());
+ updateEnd(price.timerange().end());
if (second) {
hours.append(',');
}
- hours.append(getHourFrom(price.getStartTimestamp(), zoneId));
+ hours.append(getHourFrom(price.timerange().start(), zoneId));
second = true;
}
this.hours = hours.toString();
*/
@NonNullByDefault
public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult {
-
- private List<AwattarPrice> members;
- private ZoneId zoneId;
+ private final List<AwattarPrice> members;
+ private final ZoneId zoneId;
private boolean sorted = true;
- public AwattarNonConsecutiveBestPriceResult(int size, ZoneId zoneId) {
+ public AwattarNonConsecutiveBestPriceResult(ZoneId zoneId) {
super();
this.zoneId = zoneId;
members = new ArrayList<>();
public void addMember(AwattarPrice member) {
sorted = false;
members.add(member);
- updateStart(member.getStartTimestamp());
- updateEnd(member.getEndTimestamp());
+ updateStart(member.timerange().start());
+ updateEnd(member.timerange().end());
}
@Override
public boolean isActive() {
- return members.stream().anyMatch(x -> x.contains(Instant.now().toEpochMilli()));
+ return members.stream().anyMatch(x -> x.timerange().contains(Instant.now().toEpochMilli()));
}
@Override
private void sort() {
if (!sorted) {
- members.sort(new Comparator<>() {
- @Override
- public int compare(AwattarPrice o1, AwattarPrice o2) {
- return Long.compare(o1.getStartTimestamp(), o2.getStartTimestamp());
- }
- });
+ members.sort(Comparator.comparingLong(p -> p.timerange().start()));
}
}
if (second) {
res.append(',');
}
- res.append(getHourFrom(price.getStartTimestamp(), zoneId));
+ res.append(getHourFrom(price.timerange().start(), zoneId));
second = true;
}
return res.toString();
*/
package org.openhab.binding.awattar.internal;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.awattar.internal.handler.TimeRange;
/**
* Class to store hourly price data.
*
* @author Wolfgang Klimt - initial contribution
+ * @author Jan N. Klug - Refactored to record
*/
@NonNullByDefault
-public class AwattarPrice implements Comparable<AwattarPrice> {
- private final Double price;
- private final long endTimestamp;
- private final long startTimestamp;
-
- private final int hour;
-
- public AwattarPrice(double price, long startTimestamp, long endTimestamp, ZoneId zoneId) {
- this.price = price;
- this.endTimestamp = endTimestamp;
- this.startTimestamp = startTimestamp;
- this.hour = ZonedDateTime.ofInstant(Instant.ofEpochMilli(startTimestamp), zoneId).getHour();
- }
-
- public long getStartTimestamp() {
- return startTimestamp;
- }
-
- public long getEndTimestamp() {
- return endTimestamp;
- }
-
- public double getPrice() {
- return price;
- }
+public record AwattarPrice(double netPrice, double grossPrice, double netTotal, double grossTotal,
+ TimeRange timerange) implements Comparable<AwattarPrice> {
@Override
public String toString() {
- return String.format("(%1$tF %1$tR - %2$tR: %3$.3f)", startTimestamp, endTimestamp, getPrice());
- }
-
- public int getHour() {
- return hour;
+ return String.format("(%1$tF %1$tR - %2$tR: %3$.3f)", timerange.start(), timerange.end(), netPrice);
}
@Override
public int compareTo(AwattarPrice o) {
- return price.compareTo(o.price);
- }
-
- public boolean isBetween(long start, long end) {
- return startTimestamp >= start && endTimestamp <= end;
- }
-
- public boolean contains(long timestamp) {
- return startTimestamp <= timestamp && endTimestamp > timestamp;
+ return Double.compare(netPrice, o.netPrice);
}
}
}
public static ZonedDateTime getCalendarForHour(int hour, ZoneId zone) {
- return ZonedDateTime.now(zone).truncatedTo(ChronoUnit.DAYS).plus(hour, ChronoUnit.HOURS);
+ return ZonedDateTime.now(zone).truncatedTo(ChronoUnit.DAYS).plusHours(hour);
}
public static DateTimeType getDateTimeType(long time, TimeZoneProvider tz) {
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
-import java.util.SortedMap;
+import java.util.SortedSet;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
*/
@NonNullByDefault
public class AwattarBestpriceHandler extends BaseThingHandler {
+ private static final int THING_REFRESH_INTERVAL = 60;
private final Logger logger = LoggerFactory.getLogger(AwattarBestpriceHandler.class);
- private final int thingRefreshInterval = 60;
- @Nullable
- private ScheduledFuture<?> thingRefresher;
+ private @Nullable ScheduledFuture<?> thingRefresher;
private final TimeZoneProvider timeZoneProvider;
public void initialize() {
AwattarBestpriceConfiguration config = getConfigAs(AwattarBestpriceConfiguration.class);
- boolean configValid = true;
-
if (config.length >= config.rangeDuration) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.length.value");
- configValid = false;
- }
-
- if (!configValid) {
return;
}
* here
*/
thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
- getMillisToNextMinute(1, timeZoneProvider), thingRefreshInterval * 1000, TimeUnit.MILLISECONDS);
+ getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000,
+ TimeUnit.MILLISECONDS);
}
}
updateStatus(ThingStatus.UNKNOWN);
return;
}
AwattarBridgeHandler bridgeHandler = (AwattarBridgeHandler) bridge.getHandler();
- if (bridgeHandler == null || bridgeHandler.getPriceMap() == null) {
+ if (bridgeHandler == null || bridgeHandler.getPrices() == null) {
logger.debug("No prices available, so can't refresh channel.");
// no prices available, can't continue
updateState(channelUID, state);
return;
}
AwattarBestpriceConfiguration config = getConfigAs(AwattarBestpriceConfiguration.class);
- Timerange timerange = getRange(config.rangeStart, config.rangeDuration, bridgeHandler.getTimeZone());
- if (!(bridgeHandler.containsPriceFor(timerange.start) && bridgeHandler.containsPriceFor(timerange.end))) {
+ TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, bridgeHandler.getTimeZone());
+ if (!(bridgeHandler.containsPriceFor(timerange.start()) && bridgeHandler.containsPriceFor(timerange.end()))) {
updateState(channelUID, state);
return;
}
AwattarBestPriceResult result;
+ List<AwattarPrice> range = getPriceRange(bridgeHandler, timerange);
+
if (config.consecutive) {
- ArrayList<AwattarPrice> range = new ArrayList<>(config.rangeDuration);
- range.addAll(getPriceRange(bridgeHandler, timerange,
- (o1, o2) -> Long.compare(o1.getStartTimestamp(), o2.getStartTimestamp())));
+ range.sort(Comparator.comparing(AwattarPrice::timerange));
AwattarConsecutiveBestPriceResult res = new AwattarConsecutiveBestPriceResult(
range.subList(0, config.length), bridgeHandler.getTimeZone());
}
result = res;
} else {
- List<AwattarPrice> range = getPriceRange(bridgeHandler, timerange,
- (o1, o2) -> Double.compare(o1.getPrice(), o2.getPrice()));
- AwattarNonConsecutiveBestPriceResult res = new AwattarNonConsecutiveBestPriceResult(config.length,
+ range.sort(Comparator.naturalOrder());
+ AwattarNonConsecutiveBestPriceResult res = new AwattarNonConsecutiveBestPriceResult(
bridgeHandler.getTimeZone());
int ct = 0;
for (AwattarPrice price : range) {
}
}
- private List<AwattarPrice> getPriceRange(AwattarBridgeHandler bridgeHandler, Timerange range,
- Comparator<AwattarPrice> comparator) {
- ArrayList<AwattarPrice> result = new ArrayList<>();
- SortedMap<Long, AwattarPrice> priceMap = bridgeHandler.getPriceMap();
- if (priceMap == null) {
+ private List<AwattarPrice> getPriceRange(AwattarBridgeHandler bridgeHandler, TimeRange range) {
+ List<AwattarPrice> result = new ArrayList<>();
+ SortedSet<AwattarPrice> prices = bridgeHandler.getPrices();
+ if (prices == null) {
logger.debug("No prices available, can't compute ranges");
return result;
}
- result.addAll(priceMap.values().stream().filter(x -> x.isBetween(range.start, range.end))
- .collect(Collectors.toSet()));
- result.sort(comparator);
+ result.addAll(prices.stream().filter(x -> range.contains(x.timerange())).toList());
return result;
}
- private Timerange getRange(int start, int duration, ZoneId zoneId) {
+ protected TimeRange getRange(int start, int duration, ZoneId zoneId) {
ZonedDateTime startCal = getCalendarForHour(start, zoneId);
ZonedDateTime endCal = startCal.plusHours(duration);
ZonedDateTime now = ZonedDateTime.now(zoneId);
startCal = startCal.plusDays(1);
endCal = endCal.plusDays(1);
}
- return new Timerange(startCal.toInstant().toEpochMilli(), endCal.toInstant().toEpochMilli());
- }
-
- private class Timerange {
- long start;
- long end;
-
- Timerange(long start, long end) {
- this.start = start;
- this.end = end;
- }
+ return new TimeRange(startCal.toInstant().toEpochMilli(), endCal.toInstant().toEpochMilli());
}
}
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
-import java.util.SortedMap;
-import java.util.TreeMap;
+import java.util.Comparator;
+import java.util.SortedSet;
+import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
* The {@link AwattarBridgeHandler} is responsible for retrieving data from the aWATTar API.
*
* The API provides hourly prices for the current day and, starting from 14:00, hourly prices for the next day.
- * Check the documentation at https://www.awattar.de/services/api
+ * Check the documentation at <a href="https://www.awattar.de/services/api" />
*
*
*
*/
@NonNullByDefault
public class AwattarBridgeHandler extends BaseBridgeHandler {
+ private static final int DATA_REFRESH_INTERVAL = 60;
+
private final Logger logger = LoggerFactory.getLogger(AwattarBridgeHandler.class);
private final HttpClient httpClient;
- @Nullable
- private ScheduledFuture<?> dataRefresher;
+ private @Nullable ScheduledFuture<?> dataRefresher;
private static final String URLDE = "https://api.awattar.de/v1/marketdata";
private static final String URLAT = "https://api.awattar.at/v1/marketdata";
private String url;
// This cache stores price data for up to two days
- @Nullable
- private SortedMap<Long, AwattarPrice> priceMap;
- private final int dataRefreshInterval = 60;
+ private @Nullable SortedSet<AwattarPrice> prices;
private double vatFactor = 0;
- private long lastUpdated = 0;
private double basePrice = 0;
- private long minTimestamp = 0;
- private long maxTimestamp = 0;
private ZoneId zone;
- private TimeZoneProvider timeZoneProvider;
+ private final TimeZoneProvider timeZoneProvider;
public AwattarBridgeHandler(Bridge thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
super(thing);
return;
}
- dataRefresher = scheduler.scheduleWithFixedDelay(this::refreshIfNeeded, 0, dataRefreshInterval * 1000,
+ dataRefresher = scheduler.scheduleWithFixedDelay(this::refreshIfNeeded, 0, DATA_REFRESH_INTERVAL * 1000,
TimeUnit.MILLISECONDS);
}
localRefresher.cancel(true);
}
dataRefresher = null;
- priceMap = null;
- lastUpdated = 0;
+ prices = null;
}
- public void refreshIfNeeded() {
+ void refreshIfNeeded() {
if (needRefresh()) {
refresh();
}
updateStatus(ThingStatus.ONLINE);
}
- private void getPrices() {
+ private void refresh() {
try {
// we start one day in the past to cover ranges that already started yesterday
ZonedDateTime zdt = LocalDate.now(zone).atStartOfDay(zone).minusDays(1);
String content = contentResponse.getContentAsString();
logger.trace("aWATTar API response: status = {}, content = '{}'", httpStatus, content);
- switch (httpStatus) {
- case OK_200:
- Gson gson = new Gson();
- SortedMap<Long, AwattarPrice> result = new TreeMap<>();
- minTimestamp = 0;
- maxTimestamp = 0;
- AwattarApiData apiData = gson.fromJson(content, AwattarApiData.class);
- if (apiData != null) {
- for (Datum d : apiData.data) {
- result.put(d.startTimestamp,
- new AwattarPrice(d.marketprice / 10.0, d.startTimestamp, d.endTimestamp, zone));
- updateMin(d.startTimestamp);
- updateMax(d.endTimestamp);
- }
- priceMap = result;
- updateStatus(ThingStatus.ONLINE);
- lastUpdated = Instant.now().toEpochMilli();
- } else {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
- "@text/error.invalid.data");
+ if (httpStatus == OK_200) {
+ Gson gson = new Gson();
+ SortedSet<AwattarPrice> result = new TreeSet<>(Comparator.comparing(AwattarPrice::timerange));
+ AwattarApiData apiData = gson.fromJson(content, AwattarApiData.class);
+ if (apiData != null) {
+ for (Datum d : apiData.data) {
+ double netPrice = d.marketprice / 10.0;
+ TimeRange timerange = new TimeRange(d.startTimestamp, d.endTimestamp);
+ result.add(new AwattarPrice(netPrice, netPrice * vatFactor, netPrice + basePrice,
+ (netPrice + basePrice) * vatFactor, timerange));
}
- break;
-
- default:
+ prices = result;
+ updateStatus(ThingStatus.ONLINE);
+ } else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
- "@text/warn.awattar.statuscode");
+ "@text/error.invalid.data");
+ }
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/warn.awattar.statuscode");
}
} catch (JsonSyntaxException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/error.json");
if (getThing().getStatus() != ThingStatus.ONLINE) {
return true;
}
- SortedMap<Long, AwattarPrice> localMap = priceMap;
- if (localMap == null) {
- return true;
- }
- return localMap.lastKey() < Instant.now().toEpochMilli() + 9 * 3600 * 1000;
- }
-
- private void refresh() {
- getPrices();
- }
-
- public double getVatFactor() {
- return vatFactor;
- }
-
- public double getBasePrice() {
- return basePrice;
- }
-
- public long getLastUpdated() {
- return lastUpdated;
+ SortedSet<AwattarPrice> localPrices = prices;
+ return localPrices == null
+ || localPrices.last().timerange().start() < Instant.now().toEpochMilli() + 9 * 3600 * 1000;
}
public ZoneId getTimeZone() {
}
@Nullable
- public synchronized SortedMap<Long, AwattarPrice> getPriceMap() {
- if (priceMap == null) {
+ public synchronized SortedSet<AwattarPrice> getPrices() {
+ if (prices == null) {
refresh();
}
- return priceMap;
+ return prices;
}
- @Nullable
- public AwattarPrice getPriceFor(long timestamp) {
- SortedMap<Long, AwattarPrice> priceMap = getPriceMap();
- if (priceMap == null) {
- return null;
- }
- if (!containsPriceFor(timestamp)) {
+ public @Nullable AwattarPrice getPriceFor(long timestamp) {
+ SortedSet<AwattarPrice> localPrices = getPrices();
+ if (localPrices == null || !containsPriceFor(timestamp)) {
return null;
}
- for (AwattarPrice price : priceMap.values()) {
- if (timestamp >= price.getStartTimestamp() && timestamp < price.getEndTimestamp()) {
- return price;
- }
- }
- return null;
+ return localPrices.stream().filter(e -> e.timerange().contains(timestamp)).findAny().orElse(null);
}
public boolean containsPriceFor(long timestamp) {
- return minTimestamp <= timestamp && maxTimestamp >= timestamp;
+ SortedSet<AwattarPrice> localPrices = getPrices();
+ return localPrices != null && localPrices.first().timerange().start() <= timestamp
+ && localPrices.last().timerange().end() > timestamp;
}
@Override
logger.debug("Binding {} only supports refresh command", BINDING_ID);
}
}
-
- private void updateMin(long ts) {
- minTimestamp = (minTimestamp == 0) ? ts : Math.min(minTimestamp, ts);
- }
-
- private void updateMax(long ts) {
- maxTimestamp = (maxTimestamp == 0) ? ts : Math.max(ts, maxTimestamp);
- }
}
@NonNullByDefault
@Component(configurationPid = "binding.awattar", service = ThingHandlerFactory.class)
public class AwattarHandlerFactory extends BaseThingHandlerFactory {
- private Logger logger = LoggerFactory.getLogger(AwattarHandlerFactory.class);
+ private final Logger logger = LoggerFactory.getLogger(AwattarHandlerFactory.class);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PRICE, THING_TYPE_BESTPRICE,
THING_TYPE_BRIDGE);
if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
return new AwattarBridgeHandler((Bridge) thing, httpClient, timeZoneProvider);
- }
- if (THING_TYPE_PRICE.equals(thingTypeUID)) {
+ } else if (THING_TYPE_PRICE.equals(thingTypeUID)) {
return new AwattarPriceHandler(thing, timeZoneProvider);
- }
- if (THING_TYPE_BESTPRICE.equals(thingTypeUID)) {
+ } else if (THING_TYPE_BESTPRICE.equals(thingTypeUID)) {
return new AwattarBestpriceHandler(thing, timeZoneProvider);
}
*/
@NonNullByDefault
public class AwattarPriceHandler extends BaseThingHandler {
-
+ private static final int THING_REFRESH_INTERVAL = 60;
private final Logger logger = LoggerFactory.getLogger(AwattarPriceHandler.class);
- private int thingRefreshInterval = 60;
- private TimeZoneProvider timeZoneProvider;
+ private final TimeZoneProvider timeZoneProvider;
private @Nullable ScheduledFuture<?> thingRefresher;
public AwattarPriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
/**
* Initialize the binding and start the refresh job.
- * The refresh job runs once after initialization and afterwards every hour.
+ * The refresh job runs once after initialization and afterward every hour.
*/
@Override
* here
*/
thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
- getMillisToNextMinute(1, timeZoneProvider), thingRefreshInterval * 1000, TimeUnit.MILLISECONDS);
+ getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL, TimeUnit.SECONDS);
}
}
updateStatus(ThingStatus.UNKNOWN);
if (group.equals(CHANNEL_GROUP_CURRENT)) {
target = ZonedDateTime.now(bridgeHandler.getTimeZone());
} else if (group.startsWith("today")) {
- target = getCalendarForHour(Integer.valueOf(group.substring(5)), bridgeHandler.getTimeZone());
+ target = getCalendarForHour(Integer.parseInt(group.substring(5)), bridgeHandler.getTimeZone());
} else if (group.startsWith("tomorrow")) {
- target = getCalendarForHour(Integer.valueOf(group.substring(8)), bridgeHandler.getTimeZone()).plusDays(1);
+ target = getCalendarForHour(Integer.parseInt(group.substring(8)), bridgeHandler.getTimeZone()).plusDays(1);
} else {
logger.warn("Unsupported channel group {}", group);
updateState(channelUID, state);
updateState(channelUID, state);
return;
}
- double currentprice = price.getPrice();
String channelId = channelUID.getIdWithoutGroup();
switch (channelId) {
case CHANNEL_MARKET_NET:
- state = toDecimalType(currentprice);
+ state = toDecimalType(price.netPrice());
break;
case CHANNEL_MARKET_GROSS:
- state = toDecimalType(currentprice * bridgeHandler.getVatFactor());
+ state = toDecimalType(price.grossPrice());
break;
case CHANNEL_TOTAL_NET:
- state = toDecimalType(currentprice + bridgeHandler.getBasePrice());
+ state = toDecimalType(price.netTotal());
break;
case CHANNEL_TOTAL_GROSS:
- state = toDecimalType((currentprice + bridgeHandler.getBasePrice()) * bridgeHandler.getVatFactor());
+ state = toDecimalType(price.grossTotal());
break;
default:
logger.warn("Unknown channel id {} for Thing type {}", channelUID, getThing().getThingTypeUID());
--- /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.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link TimeRange} defines a time range (defined by two timestamps)
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public record TimeRange(long start, long end) implements Comparable<TimeRange> {
+ /**
+ * Check if a given timestamp is in this time range
+ *
+ * @param timestamp the timestamp
+ * @return {@code true} if the timestamp is equal to or greater than {@link #start} and less than {@link #end}
+ */
+ public boolean contains(long timestamp) {
+ return timestamp >= start && timestamp < end;
+ }
+
+ /**
+ * Check if another time range is inside this time range
+ *
+ * @param other the other time range
+ * @return {@code true} if {@link #start} of this time range is the same or before the other time range's
+ * {@link #start} and this {@link #end} is the same or after the other time range's {@link #end}
+ */
+ public boolean contains(TimeRange other) {
+ return start <= other.start && end >= other.end;
+ }
+
+ /**
+ * Compare two time ranges by their start timestamp
+ *
+ * @param o the object to be compared
+ * @return the result of {@link Long#compare(long, long)} for the {@link #start} timestamps
+ */
+ public int compareTo(TimeRange o) {
+ return Long.compare(start, o.start);
+ }
+}
--- /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.handler;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.openhab.binding.awattar.internal.AwattarBindingConstants.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.ZoneId;
+import java.util.Map;
+import java.util.Objects;
+import java.util.SortedSet;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.openhab.binding.awattar.internal.AwattarBindingConstants;
+import org.openhab.binding.awattar.internal.AwattarPrice;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.i18n.TimeZoneProvider;
+import org.openhab.core.library.types.DateTimeType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.test.java.JavaTest;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.State;
+
+/**
+ * The {@link AwattarBridgeHandlerTest} contains tests for the {@link AwattarBridgeHandler}
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+@NonNullByDefault
+public class AwattarBridgeHandlerTest extends JavaTest {
+ public static final ThingUID BRIDGE_UID = new ThingUID(AwattarBindingConstants.THING_TYPE_BRIDGE, "testBridge");
+
+ // bridge mocks
+ private @Mock @NonNullByDefault({}) Bridge bridgeMock;
+ private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock;
+ private @Mock @NonNullByDefault({}) HttpClient httpClientMock;
+ private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
+ private @Mock @NonNullByDefault({}) Request requestMock;
+ private @Mock @NonNullByDefault({}) ContentResponse contentResponseMock;
+
+ // best price handler mocks
+ private @Mock @NonNullByDefault({}) Thing bestpriceMock;
+ private @Mock @NonNullByDefault({}) ThingHandlerCallback bestPriceCallbackMock;
+
+ private @NonNullByDefault({}) AwattarBridgeHandler bridgeHandler;
+
+ @BeforeEach
+ public void setUp() throws IOException, ExecutionException, InterruptedException, TimeoutException {
+ try (InputStream inputStream = AwattarBridgeHandlerTest.class.getResourceAsStream("api_response.json")) {
+ if (inputStream == null) {
+ throw new IOException("inputstream is null");
+ }
+ byte[] bytes = inputStream.readAllBytes();
+ if (bytes == null) {
+ throw new IOException("Resulting byte-array empty");
+ }
+ when(contentResponseMock.getContentAsString()).thenReturn(new String(bytes, StandardCharsets.UTF_8));
+ }
+ when(contentResponseMock.getStatus()).thenReturn(HttpStatus.OK_200);
+ when(httpClientMock.newRequest(anyString())).thenReturn(requestMock);
+ when(requestMock.method(HttpMethod.GET)).thenReturn(requestMock);
+ when(requestMock.timeout(10, TimeUnit.SECONDS)).thenReturn(requestMock);
+ when(requestMock.send()).thenReturn(contentResponseMock);
+
+ when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of("GMT+2"));
+
+ bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock);
+ bridgeHandler.setCallback(bridgeCallbackMock);
+ bridgeHandler.refreshIfNeeded();
+
+ when(bridgeMock.getHandler()).thenReturn(bridgeHandler);
+
+ // other mocks
+ when(bestpriceMock.getBridgeUID()).thenReturn(BRIDGE_UID);
+
+ when(bestPriceCallbackMock.getBridge(any())).thenReturn(bridgeMock);
+ when(bestPriceCallbackMock.isChannelLinked(any())).thenReturn(true);
+ }
+
+ @Test
+ public void testPricesRetrieval() {
+ SortedSet<AwattarPrice> prices = bridgeHandler.getPrices();
+
+ assertThat(prices, hasSize(72));
+
+ Objects.requireNonNull(prices);
+
+ // check if first and last element are correct
+ assertThat(prices.first().timerange().start(), is(1718316000000L));
+ assertThat(prices.last().timerange().end(), is(1718575200000L));
+ }
+
+ @Test
+ public void testGetPriceForSuccess() {
+ AwattarPrice price = bridgeHandler.getPriceFor(1718503200000L);
+
+ assertThat(price, is(notNullValue()));
+ Objects.requireNonNull(price);
+ assertThat(price.netPrice(), is(closeTo(0.219, 0.001)));
+ }
+
+ @Test
+ public void testGetPriceForFail() {
+ AwattarPrice price = bridgeHandler.getPriceFor(1518503200000L);
+
+ assertThat(price, is(nullValue()));
+ }
+
+ @Test
+ public void testContainsPrizeFor() {
+ assertThat(bridgeHandler.containsPriceFor(1618503200000L), is(false));
+ assertThat(bridgeHandler.containsPriceFor(1718503200000L), is(true));
+ assertThat(bridgeHandler.containsPriceFor(1818503200000L), is(false));
+ }
+
+ public static Stream<Arguments> testBestpriceHandler() {
+ return Stream.of( //
+ Arguments.of(1, true, CHANNEL_START, new DateTimeType("2024-06-15T14:00:00.000+0200")),
+ Arguments.of(1, true, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")),
+ Arguments.of(1, true, CHANNEL_HOURS, new StringType("14")),
+ Arguments.of(1, false, CHANNEL_START, new DateTimeType("2024-06-15T14:00:00.000+0200")),
+ Arguments.of(1, false, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")),
+ Arguments.of(1, false, CHANNEL_HOURS, new StringType("14")),
+ Arguments.of(2, true, CHANNEL_START, new DateTimeType("2024-06-15T13:00:00.000+0200")),
+ Arguments.of(2, true, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")),
+ Arguments.of(2, true, CHANNEL_HOURS, new StringType("13,14")),
+ Arguments.of(2, false, CHANNEL_START, new DateTimeType("2024-06-15T13:00:00.000+0200")),
+ Arguments.of(2, false, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")),
+ Arguments.of(2, false, CHANNEL_HOURS, new StringType("13,14")));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ public void testBestpriceHandler(int length, boolean consecutive, String channelId, State expectedState) {
+ ThingUID bestPriceUid = new ThingUID(AwattarBindingConstants.THING_TYPE_BESTPRICE, "foo");
+ Map<String, Object> config = Map.of("length", length, "consecutive", consecutive);
+ when(bestpriceMock.getConfiguration()).thenReturn(new Configuration(config));
+
+ AwattarBestpriceHandler handler = new AwattarBestpriceHandler(bestpriceMock, timeZoneProviderMock) {
+ @Override
+ protected TimeRange getRange(int start, int duration, ZoneId zoneId) {
+ return new TimeRange(1718402400000L, 1718488800000L);
+ }
+ };
+
+ handler.setCallback(bestPriceCallbackMock);
+
+ ChannelUID channelUID = new ChannelUID(bestPriceUid, channelId);
+ handler.refreshChannel(channelUID);
+ verify(bestPriceCallbackMock).stateUpdated(channelUID, expectedState);
+ }
+}
--- /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