[awattar] Refactor and add test coverage (#17752)

* [aWATTar] push test coverage and improve code readability

Signed-off-by: Thomas Leber <thomas@tl-photography.at>
This commit is contained in:
tl-photography 2025-01-15 22:02:51 +01:00 committed by GitHub
parent 968cc56452
commit baaaf7f280
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 362 additions and 120 deletions

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.binding.awattar.internal; package org.openhab.binding.awattar.internal;
import java.time.Instant;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
@ -24,7 +26,7 @@ public abstract class AwattarBestPriceResult {
private long start; private long start;
private long end; private long end;
public AwattarBestPriceResult() { protected AwattarBestPriceResult() {
} }
public long getStart() { public long getStart() {
@ -47,7 +49,18 @@ public abstract class AwattarBestPriceResult {
} }
} }
public abstract boolean isActive(); /**
* Returns true if the best price is active.
*
* @param pointInTime the current time
* @return true if the best price is active, false otherwise
*/
public abstract boolean isActive(Instant pointInTime);
/**
* Returns the hours of the best price.
*
* @return the hours of the best price as a string
*/
public abstract String getHours(); public abstract String getHours();
} }

View File

@ -76,8 +76,8 @@ public class AwattarConsecutiveBestPriceResult extends AwattarBestPriceResult {
} }
@Override @Override
public boolean isActive() { public boolean isActive(Instant pointInTime) {
return contains(Instant.now().toEpochMilli()); return contains(pointInTime.toEpochMilli());
} }
public boolean contains(long timestamp) { public boolean contains(long timestamp) {

View File

@ -32,7 +32,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult { public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult {
private final List<AwattarPrice> members; private final List<AwattarPrice> members;
private final ZoneId zoneId; private final ZoneId zoneId;
private boolean sorted = true;
public AwattarNonConsecutiveBestPriceResult(List<AwattarPrice> prices, int length, boolean inverted, public AwattarNonConsecutiveBestPriceResult(List<AwattarPrice> prices, int length, boolean inverted,
ZoneId zoneId) { ZoneId zoneId) {
@ -57,15 +56,14 @@ public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult
} }
private void addMember(AwattarPrice member) { private void addMember(AwattarPrice member) {
sorted = false;
members.add(member); members.add(member);
updateStart(member.timerange().start()); updateStart(member.timerange().start());
updateEnd(member.timerange().end()); updateEnd(member.timerange().end());
} }
@Override @Override
public boolean isActive() { public boolean isActive(Instant pointInTime) {
return members.stream().anyMatch(x -> x.timerange().contains(Instant.now().toEpochMilli())); return members.stream().anyMatch(x -> x.timerange().contains(pointInTime.toEpochMilli()));
} }
@Override @Override
@ -73,16 +71,9 @@ public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult
return String.format("NonConsecutiveBestpriceResult with %s", members.toString()); return String.format("NonConsecutiveBestpriceResult with %s", members.toString());
} }
private void sort() {
if (!sorted) {
members.sort(Comparator.comparingLong(p -> p.timerange().start()));
}
}
@Override @Override
public String getHours() { public String getHours() {
boolean second = false; boolean second = false;
sort();
StringBuilder res = new StringBuilder(); StringBuilder res = new StringBuilder();
for (AwattarPrice price : members) { for (AwattarPrice price : members) {

View File

@ -20,7 +20,6 @@ import java.time.temporal.ChronoUnit;
import javax.measure.quantity.Time; import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units; import org.openhab.core.library.unit.Units;
@ -32,9 +31,9 @@ import org.openhab.core.library.unit.Units;
@NonNullByDefault @NonNullByDefault
public class AwattarUtil { public class AwattarUtil {
public static long getMillisToNextMinute(int mod, TimeZoneProvider timeZoneProvider) { public static long getMillisToNextMinute(int mod, ZoneId zoneId) {
long now = Instant.now().toEpochMilli(); long now = Instant.now().toEpochMilli();
ZonedDateTime dt = ZonedDateTime.now(timeZoneProvider.getTimeZone()).truncatedTo(ChronoUnit.MINUTES); ZonedDateTime dt = ZonedDateTime.now(zoneId).truncatedTo(ChronoUnit.MINUTES);
int min = dt.getMinute(); int min = dt.getMinute();
int offset = min % mod; int offset = min % mod;
offset = offset == 0 ? mod : offset; offset = offset == 0 ? mod : offset;

View File

@ -15,9 +15,8 @@ package org.openhab.binding.awattar.internal.api;
import static org.eclipse.jetty.http.HttpMethod.GET; import static org.eclipse.jetty.http.HttpMethod.GET;
import static org.eclipse.jetty.http.HttpStatus.OK_200; import static org.eclipse.jetty.http.HttpStatus.OK_200;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Comparator; import java.util.Comparator;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
@ -31,6 +30,7 @@ import org.eclipse.jetty.client.api.ContentResponse;
import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration; import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration;
import org.openhab.binding.awattar.internal.AwattarPrice; import org.openhab.binding.awattar.internal.AwattarPrice;
import org.openhab.binding.awattar.internal.dto.AwattarApiData; import org.openhab.binding.awattar.internal.dto.AwattarApiData;
import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider;
import org.openhab.binding.awattar.internal.dto.Datum; import org.openhab.binding.awattar.internal.dto.Datum;
import org.openhab.binding.awattar.internal.handler.TimeRange; import org.openhab.binding.awattar.internal.handler.TimeRange;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -58,7 +58,7 @@ public class AwattarApi {
private double vatFactor; private double vatFactor;
private double basePrice; private double basePrice;
private ZoneId zone; private AwattarTimeProvider timeProvider;
private Gson gson; private Gson gson;
@ -79,8 +79,8 @@ public class AwattarApi {
* @param httpClient the HTTP client to use * @param httpClient the HTTP client to use
* @param zone the time zone to use * @param zone the time zone to use
*/ */
public AwattarApi(HttpClient httpClient, ZoneId zone, AwattarBridgeConfiguration config) { public AwattarApi(HttpClient httpClient, AwattarTimeProvider timeProvider, AwattarBridgeConfiguration config) {
this.zone = zone; this.timeProvider = timeProvider;
this.httpClient = httpClient; this.httpClient = httpClient;
this.gson = new Gson(); this.gson = new Gson();
@ -112,7 +112,7 @@ public class AwattarApi {
public SortedSet<AwattarPrice> getData() throws AwattarApiException { public SortedSet<AwattarPrice> getData() throws AwattarApiException {
try { try {
// we start one day in the past to cover ranges that already started yesterday // we start one day in the past to cover ranges that already started yesterday
ZonedDateTime zdt = LocalDate.now(zone).atStartOfDay(zone).minusDays(1); ZonedDateTime zdt = timeProvider.getZonedDateTimeNow().truncatedTo(ChronoUnit.DAYS).minusDays(1);
long start = zdt.toInstant().toEpochMilli(); long start = zdt.toInstant().toEpochMilli();
// Starting from midnight yesterday we add three days so that the range covers // Starting from midnight yesterday we add three days so that the range covers
// the whole next day. // the whole next day.

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2010-2025 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.dto;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.i18n.TimeZoneProvider;
/**
* The {@link AwattarTimeProvider} provides a time provider for aWATTar
*
* @author Thomas Leber - Initial contribution
*/
@NonNullByDefault
public class AwattarTimeProvider {
private TimeZoneProvider timeZoneProvider;
public AwattarTimeProvider(TimeZoneProvider timeZoneProvider) {
this.timeZoneProvider = timeZoneProvider;
}
/**
* Get the current zone id.
*
* @return the current zone id
*/
public ZoneId getZoneId() {
return timeZoneProvider.getTimeZone();
}
/**
* Get the current instant.
*
* @return the current instant
*/
public Instant getInstantNow() {
return Instant.now();
}
/**
* Get the current zoned date time.
*
* @return the current zoned date time
*/
public ZonedDateTime getZonedDateTimeNow() {
return Instant.now().atZone(getZoneId());
}
}

View File

@ -39,7 +39,7 @@ import org.openhab.binding.awattar.internal.AwattarBestPriceResult;
import org.openhab.binding.awattar.internal.AwattarConsecutiveBestPriceResult; import org.openhab.binding.awattar.internal.AwattarConsecutiveBestPriceResult;
import org.openhab.binding.awattar.internal.AwattarNonConsecutiveBestPriceResult; import org.openhab.binding.awattar.internal.AwattarNonConsecutiveBestPriceResult;
import org.openhab.binding.awattar.internal.AwattarPrice; import org.openhab.binding.awattar.internal.AwattarPrice;
import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider;
import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
@ -70,14 +70,13 @@ public class AwattarBestPriceHandler extends BaseThingHandler {
private static final int THING_REFRESH_INTERVAL = 60; private static final int THING_REFRESH_INTERVAL = 60;
private final Logger logger = LoggerFactory.getLogger(AwattarBestPriceHandler.class); private final Logger logger = LoggerFactory.getLogger(AwattarBestPriceHandler.class);
private final AwattarTimeProvider timeProvider;
private @Nullable ScheduledFuture<?> thingRefresher; private @Nullable ScheduledFuture<?> thingRefresher;
private final TimeZoneProvider timeZoneProvider; public AwattarBestPriceHandler(Thing thing, AwattarTimeProvider timeProvider) {
public AwattarBestPriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
super(thing); super(thing);
this.timeZoneProvider = timeZoneProvider; this.timeProvider = timeProvider;
} }
@Override @Override
@ -97,7 +96,7 @@ public class AwattarBestPriceHandler extends BaseThingHandler {
* here * here
*/ */
thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels, thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000L, getMillisToNextMinute(1, timeProvider.getZoneId()), THING_REFRESH_INTERVAL * 1000L,
TimeUnit.MILLISECONDS); TimeUnit.MILLISECONDS);
} }
} }
@ -141,7 +140,7 @@ public class AwattarBestPriceHandler extends BaseThingHandler {
return; return;
} }
ZoneId zoneId = bridgeHandler.getTimeZone(); ZoneId zoneId = timeProvider.getZoneId();
AwattarBestPriceConfiguration config = getConfigAs(AwattarBestPriceConfiguration.class); AwattarBestPriceConfiguration config = getConfigAs(AwattarBestPriceConfiguration.class);
TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, zoneId); TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, zoneId);
@ -163,7 +162,7 @@ public class AwattarBestPriceHandler extends BaseThingHandler {
long diff; long diff;
switch (channelId) { switch (channelId) {
case CHANNEL_ACTIVE: case CHANNEL_ACTIVE:
state = OnOffType.from(result.isActive()); state = OnOffType.from(result.isActive(timeProvider.getInstantNow()));
break; break;
case CHANNEL_START: case CHANNEL_START:
state = new DateTimeType(Instant.ofEpochMilli(result.getStart())); state = new DateTimeType(Instant.ofEpochMilli(result.getStart()));
@ -172,7 +171,7 @@ public class AwattarBestPriceHandler extends BaseThingHandler {
state = new DateTimeType(Instant.ofEpochMilli(result.getEnd())); state = new DateTimeType(Instant.ofEpochMilli(result.getEnd()));
break; break;
case CHANNEL_COUNTDOWN: case CHANNEL_COUNTDOWN:
diff = result.getStart() - Instant.now().toEpochMilli(); diff = result.getStart() - timeProvider.getInstantNow().toEpochMilli();
if (diff >= 0) { if (diff >= 0) {
state = getDuration(diff); state = getDuration(diff);
} else { } else {
@ -180,8 +179,8 @@ public class AwattarBestPriceHandler extends BaseThingHandler {
} }
break; break;
case CHANNEL_REMAINING: case CHANNEL_REMAINING:
if (result.isActive()) { if (result.isActive(timeProvider.getInstantNow())) {
diff = result.getEnd() - Instant.now().toEpochMilli(); diff = result.getEnd() - timeProvider.getInstantNow().toEpochMilli();
state = getDuration(diff); state = getDuration(diff);
} else { } else {
state = QuantityType.valueOf(0, Units.MINUTE); state = QuantityType.valueOf(0, Units.MINUTE);
@ -216,20 +215,39 @@ public class AwattarBestPriceHandler extends BaseThingHandler {
return result; return result;
} }
/**
* Returns the time range for the given start hour and duration.
*
* @param start the start hour (0-23)
* @param duration the duration in hours
* @param zoneId the time zone to use
* @return the range
*/
protected TimeRange getRange(int start, int duration, ZoneId zoneId) { protected TimeRange getRange(int start, int duration, ZoneId zoneId) {
ZonedDateTime startCal = getCalendarForHour(start, zoneId); ZonedDateTime startTime = getStartTime(start, zoneId);
ZonedDateTime endCal = startCal.plusHours(duration); ZonedDateTime endTime = startTime.plusHours(duration);
ZonedDateTime now = ZonedDateTime.now(zoneId); ZonedDateTime now = timeProvider.getZonedDateTimeNow();
if (now.getHour() < start) { if (now.getHour() < start) {
// we are before the range, so we might be still within the last range // we are before the range, so we might be still within the last range
startCal = startCal.minusDays(1); startTime = startTime.minusDays(1);
endCal = endCal.minusDays(1); endTime = endTime.minusDays(1);
} }
if (endCal.toInstant().toEpochMilli() < Instant.now().toEpochMilli()) { if (endTime.isBefore(now)) {
// span is in the past, add one day // span is in the past, add one day
startCal = startCal.plusDays(1); startTime = startTime.plusDays(1);
endCal = endCal.plusDays(1); endTime = endTime.plusDays(1);
} }
return new TimeRange(startCal.toInstant().toEpochMilli(), endCal.toInstant().toEpochMilli()); return new TimeRange(startTime.toInstant().toEpochMilli(), endTime.toInstant().toEpochMilli());
}
/**
* Returns the start time for the given hour.
*
* @param start the hour. Must be between 0 and 23.
* @param zoneId the time zone
* @return the start time
*/
protected ZonedDateTime getStartTime(int start, ZoneId zoneId) {
return getCalendarForHour(start, zoneId);
} }
} }

View File

@ -16,12 +16,11 @@ import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANN
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_TOTAL_NET; import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_TOTAL_NET;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Function; import java.util.function.ToDoubleFunction;
import javax.measure.Unit; import javax.measure.Unit;
@ -32,7 +31,7 @@ import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration;
import org.openhab.binding.awattar.internal.AwattarPrice; import org.openhab.binding.awattar.internal.AwattarPrice;
import org.openhab.binding.awattar.internal.api.AwattarApi; import org.openhab.binding.awattar.internal.api.AwattarApi;
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException; import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.CurrencyUnits; import org.openhab.core.library.unit.CurrencyUnits;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
@ -65,20 +64,20 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(AwattarBridgeHandler.class); private final Logger logger = LoggerFactory.getLogger(AwattarBridgeHandler.class);
private final HttpClient httpClient; private final HttpClient httpClient;
private final AwattarTimeProvider timeProvider;
private @Nullable ScheduledFuture<?> dataRefresher; private @Nullable ScheduledFuture<?> dataRefresher;
private Instant lastRefresh = Instant.EPOCH; private Instant lastRefresh = Instant.EPOCH;
// This cache stores price data for up to two days // This cache stores price data for up to two days
private @Nullable SortedSet<AwattarPrice> prices; private @Nullable SortedSet<AwattarPrice> prices;
private ZoneId zone;
private @Nullable AwattarApi awattarApi; private @Nullable AwattarApi awattarApi;
public AwattarBridgeHandler(Bridge thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) { public AwattarBridgeHandler(Bridge thing, HttpClient httpClient, AwattarTimeProvider timeProvider) {
super(thing); super(thing);
this.httpClient = httpClient; this.httpClient = httpClient;
zone = timeZoneProvider.getTimeZone(); this.timeProvider = timeProvider;
} }
@Override @Override
@ -87,7 +86,7 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
AwattarBridgeConfiguration config = getConfigAs(AwattarBridgeConfiguration.class); AwattarBridgeConfiguration config = getConfigAs(AwattarBridgeConfiguration.class);
try { try {
awattarApi = new AwattarApi(httpClient, zone, config); awattarApi = new AwattarApi(httpClient, timeProvider, config);
dataRefresher = scheduler.scheduleWithFixedDelay(this::refreshIfNeeded, 0, DATA_REFRESH_INTERVAL * 1000L, dataRefresher = scheduler.scheduleWithFixedDelay(this::refreshIfNeeded, 0, DATA_REFRESH_INTERVAL * 1000L,
TimeUnit.MILLISECONDS); TimeUnit.MILLISECONDS);
@ -154,17 +153,15 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
return priceUnit; return priceUnit;
} }
private void createAndSendTimeSeries(String channelId, Function<AwattarPrice, Double> valueFunction) { private void createAndSendTimeSeries(String channelId, ToDoubleFunction<AwattarPrice> valueFunction) {
SortedSet<AwattarPrice> locPrices = getPrices(); SortedSet<AwattarPrice> locPrices = getPrices();
Unit<?> priceUnit = getPriceUnit(); Unit<?> priceUnit = getPriceUnit();
if (locPrices == null) { if (locPrices == null) {
return; return;
} }
TimeSeries timeSeries = new TimeSeries(TimeSeries.Policy.REPLACE); TimeSeries timeSeries = new TimeSeries(TimeSeries.Policy.REPLACE);
locPrices.forEach(p -> { locPrices.forEach(p -> timeSeries.add(Instant.ofEpochMilli(p.timerange().start()),
timeSeries.add(Instant.ofEpochMilli(p.timerange().start()), new QuantityType<>(valueFunction.applyAsDouble(p) / 100.0, priceUnit)));
new QuantityType<>(valueFunction.apply(p) / 100.0, priceUnit));
});
sendTimeSeries(channelId, timeSeries); sendTimeSeries(channelId, timeSeries);
} }
@ -186,11 +183,13 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
private boolean needRefresh() { private boolean needRefresh() {
// if the thing is offline, we need to refresh // if the thing is offline, we need to refresh
if (getThing().getStatus() != ThingStatus.ONLINE) { if (getThing().getStatus() != ThingStatus.ONLINE) {
lastRefresh = timeProvider.getInstantNow();
return true; return true;
} }
// if the local cache is empty, we need to refresh // if the local cache is empty, we need to refresh
if (prices == null) { if (prices == null) {
lastRefresh = timeProvider.getInstantNow();
return true; return true;
} }
@ -200,15 +199,15 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
// do not refresh before 15:00, since the prices for the next day are available // do not refresh before 15:00, since the prices for the next day are available
// only after 14:00 // only after 14:00
ZonedDateTime now = ZonedDateTime.now(zone); ZonedDateTime now = timeProvider.getZonedDateTimeNow();
if (now.getHour() < 15) { if (now.getHour() < 15) {
return false; return false;
} }
// refresh then every 3 hours, if the last refresh was more than an hour ago // refresh at 15:00, 18:00 and 21:00 if the last refresh was more than an hour ago
if (now.getHour() % 3 == 0 && lastRefresh.getEpochSecond() < now.minusHours(1).toEpochSecond()) { if (now.getHour() % 3 == 0 && lastRefresh.getEpochSecond() < now.minusHours(1).toEpochSecond()) {
// update the last refresh time // update the last refresh time
lastRefresh = Instant.now(); lastRefresh = timeProvider.getInstantNow();
// return true to indicate an update is needed // return true to indicate an update is needed
return true; return true;
@ -217,10 +216,6 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
return false; return false;
} }
public ZoneId getTimeZone() {
return zone;
}
@Nullable @Nullable
public synchronized SortedSet<AwattarPrice> getPrices() { public synchronized SortedSet<AwattarPrice> getPrices() {
if (prices == null) { if (prices == null) {
@ -261,6 +256,7 @@ public class AwattarBridgeHandler extends BaseBridgeHandler {
switch (channelUID.getId()) { switch (channelUID.getId()) {
case CHANNEL_MARKET_NET -> createAndSendTimeSeries(CHANNEL_MARKET_NET, AwattarPrice::netPrice); case CHANNEL_MARKET_NET -> createAndSendTimeSeries(CHANNEL_MARKET_NET, AwattarPrice::netPrice);
case CHANNEL_TOTAL_NET -> createAndSendTimeSeries(CHANNEL_TOTAL_NET, AwattarPrice::netTotal); case CHANNEL_TOTAL_NET -> createAndSendTimeSeries(CHANNEL_TOTAL_NET, AwattarPrice::netTotal);
default -> logger.warn("Channel {} not supported", channelUID.getId());
} }
} }
} }

View File

@ -21,6 +21,7 @@ import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider;
import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
@ -50,13 +51,13 @@ public class AwattarHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PRICE, THING_TYPE_BESTPRICE, private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PRICE, THING_TYPE_BESTPRICE,
THING_TYPE_BRIDGE); THING_TYPE_BRIDGE);
private final HttpClient httpClient; private final HttpClient httpClient;
private final TimeZoneProvider timeZoneProvider; private final AwattarTimeProvider timeProvider;
@Activate @Activate
public AwattarHandlerFactory(final @Reference HttpClientFactory httpClientFactory, public AwattarHandlerFactory(final @Reference HttpClientFactory httpClientFactory,
final @Reference TimeZoneProvider timeZoneProvider) { final @Reference TimeZoneProvider timeZoneProvider) {
this.httpClient = httpClientFactory.getCommonHttpClient(); this.httpClient = httpClientFactory.getCommonHttpClient();
this.timeZoneProvider = timeZoneProvider; this.timeProvider = new AwattarTimeProvider(timeZoneProvider);
} }
@Override @Override
@ -69,11 +70,11 @@ public class AwattarHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID(); ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { if (THING_TYPE_BRIDGE.equals(thingTypeUID)) {
return new AwattarBridgeHandler((Bridge) thing, httpClient, timeZoneProvider); return new AwattarBridgeHandler((Bridge) thing, httpClient, timeProvider);
} else if (THING_TYPE_PRICE.equals(thingTypeUID)) { } else if (THING_TYPE_PRICE.equals(thingTypeUID)) {
return new AwattarPriceHandler(thing, timeZoneProvider); return new AwattarPriceHandler(thing, timeProvider);
} else if (THING_TYPE_BESTPRICE.equals(thingTypeUID)) { } else if (THING_TYPE_BESTPRICE.equals(thingTypeUID)) {
return new AwattarBestPriceHandler(thing, timeZoneProvider); return new AwattarBestPriceHandler(thing, timeProvider);
} }
logger.warn("Unknown thing type {}, not creating handler!", thingTypeUID); logger.warn("Unknown thing type {}, not creating handler!", thingTypeUID);

View File

@ -30,7 +30,7 @@ import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.awattar.internal.AwattarPrice; import org.openhab.binding.awattar.internal.AwattarPrice;
import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
@ -55,15 +55,16 @@ import org.slf4j.LoggerFactory;
*/ */
@NonNullByDefault @NonNullByDefault
public class AwattarPriceHandler extends BaseThingHandler { public class AwattarPriceHandler extends BaseThingHandler {
private final AwattarTimeProvider timeProvider;
private static final int THING_REFRESH_INTERVAL = 60; private static final int THING_REFRESH_INTERVAL = 60;
private final Logger logger = LoggerFactory.getLogger(AwattarPriceHandler.class); private final Logger logger = LoggerFactory.getLogger(AwattarPriceHandler.class);
private final TimeZoneProvider timeZoneProvider;
private @Nullable ScheduledFuture<?> thingRefresher; private @Nullable ScheduledFuture<?> thingRefresher;
public AwattarPriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) { public AwattarPriceHandler(Thing thing, AwattarTimeProvider timeProvider) {
super(thing); super(thing);
this.timeZoneProvider = timeZoneProvider; this.timeProvider = timeProvider;
} }
@Override @Override
@ -90,7 +91,7 @@ public class AwattarPriceHandler extends BaseThingHandler {
* here * here
*/ */
thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels, thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000, getMillisToNextMinute(1, timeProvider.getZoneId()), THING_REFRESH_INTERVAL * 1000L,
TimeUnit.MILLISECONDS); TimeUnit.MILLISECONDS);
} }
} }
@ -139,11 +140,11 @@ public class AwattarPriceHandler extends BaseThingHandler {
ZonedDateTime target; ZonedDateTime target;
if (group.equals(CHANNEL_GROUP_CURRENT)) { if (group.equals(CHANNEL_GROUP_CURRENT)) {
target = ZonedDateTime.now(bridgeHandler.getTimeZone()); target = timeProvider.getZonedDateTimeNow();
} else if (group.startsWith("today")) { } else if (group.startsWith("today")) {
target = getCalendarForHour(Integer.parseInt(group.substring(5)), bridgeHandler.getTimeZone()); target = getCalendarForHour(Integer.parseInt(group.substring(5)), timeProvider.getZoneId());
} else if (group.startsWith("tomorrow")) { } else if (group.startsWith("tomorrow")) {
target = getCalendarForHour(Integer.parseInt(group.substring(8)), bridgeHandler.getTimeZone()).plusDays(1); target = getCalendarForHour(Integer.parseInt(group.substring(8)), timeProvider.getZoneId()).plusDays(1);
} else { } else {
logger.warn("Unsupported channel group {}", group); logger.warn("Unsupported channel group {}", group);
updateState(channelUID, state); updateState(channelUID, state);
@ -153,7 +154,7 @@ public class AwattarPriceHandler extends BaseThingHandler {
AwattarPrice price = bridgeHandler.getPriceFor(target.toInstant().toEpochMilli()); AwattarPrice price = bridgeHandler.getPriceFor(target.toInstant().toEpochMilli());
if (price == null) { if (price == null) {
logger.trace("No price found for hour {}", target.toString()); logger.trace("No price found for hour {}", target);
updateState(channelUID, state); updateState(channelUID, state);
return; return;
} }

View File

@ -24,6 +24,7 @@ import java.util.List;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openhab.binding.awattar.internal.handler.TimeRange; import org.openhab.binding.awattar.internal.handler.TimeRange;
@ -33,6 +34,7 @@ import org.openhab.binding.awattar.internal.handler.TimeRange;
* *
* @author Thomas Leber - Initial contribution * @author Thomas Leber - Initial contribution
*/ */
@NonNullByDefault
public class AwattarBestPriceTest { public class AwattarBestPriceTest {
private ZoneId zoneId = ZoneId.of("GMT"); private ZoneId zoneId = ZoneId.of("GMT");
@ -74,7 +76,7 @@ public class AwattarBestPriceTest {
} }
@Test @Test
void AwattarConsecutiveBestPriceResult() { void awattarConsecutiveBestPriceResult() {
int length = 8; int length = 8;
List<AwattarPrice> range = new ArrayList<>(getPrices()); List<AwattarPrice> range = new ArrayList<>(getPrices());
@ -85,7 +87,7 @@ public class AwattarBestPriceTest {
} }
@Test @Test
void AwattarNonConsecutiveBestPriceResult_nonInverted() { void awattarNonConsecutiveBestPriceResultNonInverted() {
int length = 6; int length = 6;
boolean inverted = false; boolean inverted = false;
@ -98,7 +100,7 @@ public class AwattarBestPriceTest {
} }
@Test @Test
void AwattarNonConsecutiveBestPriceResult_inverted() { void awattarNonConsecutiveBestPriceResultInverted() {
int length = 4; int length = 4;
boolean inverted = true; boolean inverted = true;

View File

@ -17,12 +17,16 @@ import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Objects; import java.util.Objects;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -45,7 +49,7 @@ import org.mockito.quality.Strictness;
import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration; import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration;
import org.openhab.binding.awattar.internal.AwattarPrice; import org.openhab.binding.awattar.internal.AwattarPrice;
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException; import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider;
import org.openhab.core.test.java.JavaTest; import org.openhab.core.test.java.JavaTest;
/** /**
@ -60,10 +64,10 @@ import org.openhab.core.test.java.JavaTest;
class AwattarApiTest extends JavaTest { class AwattarApiTest extends JavaTest {
// API Mocks // API Mocks
private @Mock @NonNullByDefault({}) HttpClient httpClientMock; private @Mock @NonNullByDefault({}) HttpClient httpClientMock;
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
private @Mock @NonNullByDefault({}) Request requestMock; private @Mock @NonNullByDefault({}) Request requestMock;
private @Mock @NonNullByDefault({}) ContentResponse contentResponseMock; private @Mock @NonNullByDefault({}) ContentResponse contentResponseMock;
private @Mock @NonNullByDefault({}) AwattarBridgeConfiguration config; private @Mock @NonNullByDefault({}) AwattarBridgeConfiguration config;
private @Mock @NonNullByDefault({}) AwattarTimeProvider timeProviderMock;
// sut // sut
private @NonNullByDefault({}) AwattarApi api; private @NonNullByDefault({}) AwattarApi api;
@ -86,30 +90,33 @@ class AwattarApiTest extends JavaTest {
when(requestMock.timeout(10, TimeUnit.SECONDS)).thenReturn(requestMock); when(requestMock.timeout(10, TimeUnit.SECONDS)).thenReturn(requestMock);
when(requestMock.send()).thenReturn(contentResponseMock); when(requestMock.send()).thenReturn(contentResponseMock);
when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of("GMT+2")); ZonedDateTime zdt = Instant.parse("2024-06-15T12:00:00Z").atZone(ZoneId.of("GMT+2"));
when(timeProviderMock.getZonedDateTimeNow()).thenReturn(zdt);
config.basePrice = 0.0; config.basePrice = 0.0;
config.vatPercent = 0.0; config.vatPercent = 0.0;
config.country = "DE"; config.country = "DE";
api = new AwattarApi(httpClientMock, ZoneId.of("GMT+2"), config); api = new AwattarApi(httpClientMock, timeProviderMock, config);
} }
@Test @Test
void testDeUrl() throws AwattarApiException { void testDeUrl() throws AwattarApiException {
api.getData(); api.getData();
assertThat(httpClientMock.newRequest("https://api.awattar.de/v1/marketdata"), is(requestMock)); verify(httpClientMock, times(1))
.newRequest("https://api.awattar.de/v1/marketdata?start=1718316000000&end=1718575200000");
} }
@Test @Test
void testAtUrl() throws AwattarApiException { void testAtUrl() throws AwattarApiException {
config.country = "AT"; config.country = "AT";
api = new AwattarApi(httpClientMock, ZoneId.of("GMT+2"), config); api = new AwattarApi(httpClientMock, timeProviderMock, config);
api.getData(); api.getData();
assertThat(httpClientMock.newRequest("https://api.awattar.at/v1/marketdata"), is(requestMock)); verify(httpClientMock, times(1))
.newRequest("https://api.awattar.at/v1/marketdata?start=1718316000000&end=1718575200000");
} }
@Test @Test
@ -117,7 +124,7 @@ class AwattarApiTest extends JavaTest {
config.country = "CH"; config.country = "CH";
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
() -> new AwattarApi(httpClientMock, ZoneId.of("GMT+2"), config)); () -> new AwattarApi(httpClientMock, timeProviderMock, config));
assertThat(thrown.getMessage(), is("Country code must be 'DE' or 'AT'")); assertThat(thrown.getMessage(), is("Country code must be 'DE' or 'AT'"));
} }

View File

@ -19,14 +19,20 @@ import static org.mockito.Mockito.when;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.platform.commons.support.HierarchyTraversalMode; import org.junit.platform.commons.support.HierarchyTraversalMode;
import org.junit.platform.commons.support.ReflectionSupport; import org.junit.platform.commons.support.ReflectionSupport;
import org.mockito.Mock; import org.mockito.Mock;
@ -36,7 +42,7 @@ import org.mockito.quality.Strictness;
import org.openhab.binding.awattar.internal.AwattarBindingConstants; import org.openhab.binding.awattar.internal.AwattarBindingConstants;
import org.openhab.binding.awattar.internal.api.AwattarApi; import org.openhab.binding.awattar.internal.api.AwattarApi;
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException; import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider;
import org.openhab.core.test.java.JavaTest; import org.openhab.core.test.java.JavaTest;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
@ -62,8 +68,9 @@ class AwattarBridgeHandlerRefreshTest extends JavaTest {
private @Mock @NonNullByDefault({}) Bridge bridgeMock; private @Mock @NonNullByDefault({}) Bridge bridgeMock;
private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock; private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock;
private @Mock @NonNullByDefault({}) HttpClient httpClientMock; private @Mock @NonNullByDefault({}) HttpClient httpClientMock;
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock; private @Mock @NonNullByDefault({}) Clock fixedClock;
private @Mock @NonNullByDefault({}) AwattarApi awattarApiMock; private @Mock @NonNullByDefault({}) AwattarApi awattarApiMock;
private @Mock @NonNullByDefault({}) AwattarTimeProvider timeProviderMock;
// best price handler mocks // best price handler mocks
private @Mock @NonNullByDefault({}) Thing bestpriceMock; private @Mock @NonNullByDefault({}) Thing bestpriceMock;
@ -73,10 +80,11 @@ class AwattarBridgeHandlerRefreshTest extends JavaTest {
@BeforeEach @BeforeEach
public void setUp() throws IllegalArgumentException, IllegalAccessException { public void setUp() throws IllegalArgumentException, IllegalAccessException {
when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of("GMT+2"));
when(bridgeMock.getUID()).thenReturn(BRIDGE_UID); when(bridgeMock.getUID()).thenReturn(BRIDGE_UID);
bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock);
when(timeProviderMock.getZoneId()).thenReturn(ZoneId.of("GMT+2"));
bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeProviderMock);
bridgeHandler.setCallback(bridgeCallbackMock); bridgeHandler.setCallback(bridgeCallbackMock);
List<Field> fields = ReflectionSupport.findFields(AwattarBridgeHandler.class, List<Field> fields = ReflectionSupport.findFields(AwattarBridgeHandler.class,
@ -95,7 +103,7 @@ class AwattarBridgeHandlerRefreshTest extends JavaTest {
* @throws AwattarApiException * @throws AwattarApiException
*/ */
@Test @Test
void testRefreshIfNeeded_ThingOffline() throws SecurityException, AwattarApiException { void testRefreshIfNeededThingOffline() throws SecurityException, AwattarApiException {
when(bridgeMock.getStatus()).thenReturn(ThingStatus.OFFLINE); when(bridgeMock.getStatus()).thenReturn(ThingStatus.OFFLINE);
bridgeHandler.refreshIfNeeded(); bridgeHandler.refreshIfNeeded();
@ -113,7 +121,7 @@ class AwattarBridgeHandlerRefreshTest extends JavaTest {
* @throws AwattarApiException * @throws AwattarApiException
*/ */
@Test @Test
void testRefreshIfNeeded_DataEmpty() throws SecurityException, AwattarApiException { void testRefreshIfNeededDataEmpty() throws SecurityException, AwattarApiException {
when(bridgeMock.getStatus()).thenReturn(ThingStatus.ONLINE); when(bridgeMock.getStatus()).thenReturn(ThingStatus.ONLINE);
bridgeHandler.refreshIfNeeded(); bridgeHandler.refreshIfNeeded();
@ -124,7 +132,7 @@ class AwattarBridgeHandlerRefreshTest extends JavaTest {
} }
@Test @Test
void testNeedRefresh_ThingOffline() throws SecurityException { void testNeedRefreshThingOffline() throws SecurityException {
when(bridgeMock.getStatus()).thenReturn(ThingStatus.OFFLINE); when(bridgeMock.getStatus()).thenReturn(ThingStatus.OFFLINE);
// get private method via reflection // get private method via reflection
@ -136,7 +144,7 @@ class AwattarBridgeHandlerRefreshTest extends JavaTest {
} }
@Test @Test
void testNeedRefresh_DataEmpty() throws SecurityException, IllegalArgumentException, IllegalAccessException { void testNeedRefreshDataEmpty() throws SecurityException, IllegalArgumentException, IllegalAccessException {
when(bridgeMock.getStatus()).thenReturn(ThingStatus.ONLINE); when(bridgeMock.getStatus()).thenReturn(ThingStatus.ONLINE);
List<Field> fields = ReflectionSupport.findFields(AwattarBridgeHandler.class, List<Field> fields = ReflectionSupport.findFields(AwattarBridgeHandler.class,
@ -154,4 +162,68 @@ class AwattarBridgeHandlerRefreshTest extends JavaTest {
assertThat(result, is(true)); assertThat(result, is(true));
} }
public static Stream<Arguments> testNeedRefreshTimes() {
return Stream.of(
// Update at 15:00 GMT+2
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T13:00:00Z"), true),
Arguments.of(Instant.parse("2021-01-01T12:00:00Z"), Instant.parse("2021-01-01T13:00:00Z"), false),
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T13:30:00Z"), true),
Arguments.of(Instant.parse("2021-01-01T12:00:00Z"), Instant.parse("2021-01-01T13:30:00Z"), true),
// Update at 16:00 GMT+2 and 17:00 GMT+2
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T14:00:00Z"), false),
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T15:00:00Z"), false),
// Update at 18:00 GMT+2
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T16:00:00Z"), true),
Arguments.of(Instant.parse("2021-01-01T15:00:00Z"), Instant.parse("2021-01-01T16:00:00Z"), false),
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T16:30:00Z"), true),
Arguments.of(Instant.parse("2021-01-01T15:00:00Z"), Instant.parse("2021-01-01T16:30:00Z"), true),
// Update at 19:00 GMT+2 and 20:00 GMT+2
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T17:00:00Z"), false),
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T18:00:00Z"), false),
// Update at 21:00 GMT+2
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T19:00:00Z"), true),
Arguments.of(Instant.parse("2021-01-01T18:00:00Z"), Instant.parse("2021-01-01T19:00:00Z"), false),
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T19:30:00Z"), true),
Arguments.of(Instant.parse("2021-01-01T18:00:00Z"), Instant.parse("2021-01-01T19:30:00Z"), true),
// Update at 22:00 GMT+2, 23:00 GMT+2 and 00:00 GMT+2
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T20:00:00Z"), false),
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T21:00:00Z"), false),
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T22:00:00Z"), false),
// Update before 15:00 GMT+2
Arguments.of(Instant.parse("2021-01-01T11:00:00Z"), Instant.parse("2021-01-01T14:00:00Z"), false),
Arguments.of(Instant.parse("2021-01-01T10:59:58Z"), Instant.parse("2021-01-01T11:59:59Z"), false),
Arguments.of(Instant.parse("2021-01-01T11:59:59Z"), Instant.parse("2021-01-01T11:59:59Z"), false),
Arguments.of(Instant.parse("2021-01-01T13:00:00Z"), Instant.parse("2021-01-01T13:00:00Z"), false));
}
@ParameterizedTest
@MethodSource
void testNeedRefreshTimes(Instant lastUpdate, Instant nowUpdate, Boolean expectedResult) {
when(bridgeMock.getStatus()).thenReturn(ThingStatus.ONLINE);
fixedClock = Clock.fixed(lastUpdate, ZoneId.of("GMT+2"));
when(timeProviderMock.getZoneId()).thenReturn(fixedClock.getZone());
when(timeProviderMock.getInstantNow()).thenReturn(fixedClock.instant());
bridgeHandler.refreshIfNeeded();
fixedClock = Clock.fixed(nowUpdate, ZoneId.of("GMT+2"));
when(timeProviderMock.getZoneId()).thenReturn(fixedClock.getZone());
when(timeProviderMock.getInstantNow()).thenReturn(fixedClock.instant());
when(timeProviderMock.getZonedDateTimeNow()).thenReturn(fixedClock.instant().atZone(fixedClock.getZone()));
// get private method via reflection
Method method = ReflectionSupport.findMethod(AwattarBridgeHandler.class, "needRefresh", "").get();
boolean result = (boolean) ReflectionSupport.invokeMethod(method, bridgeHandler);
assertThat(result, is(expectedResult));
}
} }

View File

@ -20,14 +20,20 @@ import static org.hamcrest.Matchers.nullValue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_ACTIVE;
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_COUNTDOWN;
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_END; 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_HOURS;
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_REMAINING;
import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_START; import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_START;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -55,9 +61,11 @@ import org.openhab.binding.awattar.internal.AwattarPrice;
import org.openhab.binding.awattar.internal.api.AwattarApi; import org.openhab.binding.awattar.internal.api.AwattarApi;
import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException; import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException;
import org.openhab.binding.awattar.internal.dto.AwattarApiData; import org.openhab.binding.awattar.internal.dto.AwattarApiData;
import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider;
import org.openhab.core.config.core.Configuration; 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.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
import org.openhab.core.test.java.JavaTest; import org.openhab.core.test.java.JavaTest;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
@ -84,8 +92,8 @@ public class AwattarBridgeHandlerTest extends JavaTest {
private @Mock @NonNullByDefault({}) Bridge bridgeMock; private @Mock @NonNullByDefault({}) Bridge bridgeMock;
private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock; private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock;
private @Mock @NonNullByDefault({}) HttpClient httpClientMock; private @Mock @NonNullByDefault({}) HttpClient httpClientMock;
private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock;
private @Mock @NonNullByDefault({}) AwattarApi awattarApiMock; private @Mock @NonNullByDefault({}) AwattarApi awattarApiMock;
private @Mock @NonNullByDefault({}) AwattarTimeProvider timeProviderMock;
// best price handler mocks // best price handler mocks
private @Mock @NonNullByDefault({}) Thing bestpriceMock; private @Mock @NonNullByDefault({}) Thing bestpriceMock;
@ -109,10 +117,8 @@ public class AwattarBridgeHandlerTest extends JavaTest {
when(awattarApiMock.getData()).thenReturn(result); when(awattarApiMock.getData()).thenReturn(result);
} }
when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of("GMT+2"));
when(bridgeMock.getUID()).thenReturn(BRIDGE_UID); when(bridgeMock.getUID()).thenReturn(BRIDGE_UID);
bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock); bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeProviderMock);
bridgeHandler.setCallback(bridgeCallbackMock); bridgeHandler.setCallback(bridgeCallbackMock);
// mock the private field awattarApi // mock the private field awattarApi
@ -166,31 +172,105 @@ public class AwattarBridgeHandlerTest extends JavaTest {
public static Stream<Arguments> testBestpriceHandler() { public static Stream<Arguments> testBestpriceHandler() {
return Stream.of( // return Stream.of( //
Arguments.of(1, true, CHANNEL_START, new DateTimeType("2024-06-15T14:00:00.000+0200")), Arguments.of(24, 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(24, 1, true, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")),
Arguments.of(1, true, CHANNEL_HOURS, new StringType("14")), Arguments.of(24, 1, true, CHANNEL_HOURS, new StringType("14")),
Arguments.of(1, false, CHANNEL_START, new DateTimeType("2024-06-15T14:00:00.000+0200")), Arguments.of(24, 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(24, 1, false, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")),
Arguments.of(1, false, CHANNEL_HOURS, new StringType("14")), Arguments.of(24, 1, false, CHANNEL_HOURS, new StringType("14")),
Arguments.of(2, true, CHANNEL_START, new DateTimeType("2024-06-15T13:00:00.000+0200")), Arguments.of(24, 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(24, 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(24, 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(24, 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(24, 2, false, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")),
Arguments.of(2, false, CHANNEL_HOURS, new StringType("13,14"))); Arguments.of(24, 2, false, CHANNEL_HOURS, new StringType("13,14")),
Arguments.of(34, 4, false, CHANNEL_START, new DateTimeType("2024-06-15T12:00:00.000+0200")),
Arguments.of(34, 4, false, CHANNEL_END, new DateTimeType("2024-06-15T16:00:00.000+0200")),
Arguments.of(34, 4, false, CHANNEL_HOURS, new StringType("12,13,14,15")),
Arguments.of(34, 8, false, CHANNEL_START, new DateTimeType("2024-06-15T12:00:00.000+0200")),
Arguments.of(34, 8, false, CHANNEL_END, new DateTimeType("2024-06-16T16:00:00.000+0200")),
Arguments.of(34, 8, false, CHANNEL_HOURS, new StringType("12,13,14,15,16,13,14,15")));
} }
@ParameterizedTest @ParameterizedTest
@MethodSource @MethodSource
void testBestpriceHandler(int length, boolean consecutive, String channelId, State expectedState) { void testBestpriceHandler(int rangeDuration, int length, boolean consecutive, String channelId,
State expectedState) {
ThingUID bestPriceUid = new ThingUID(AwattarBindingConstants.THING_TYPE_BESTPRICE, "foo"); ThingUID bestPriceUid = new ThingUID(AwattarBindingConstants.THING_TYPE_BESTPRICE, "foo");
Map<String, Object> config = Map.of("length", length, "consecutive", consecutive); Map<String, Object> config = Map.of("rangeDuration", rangeDuration, "length", length, "consecutive",
consecutive);
when(bestpriceMock.getConfiguration()).thenReturn(new Configuration(config)); when(bestpriceMock.getConfiguration()).thenReturn(new Configuration(config));
AwattarBestPriceHandler handler = new AwattarBestPriceHandler(bestpriceMock, timeZoneProviderMock) { Clock clock = Clock.fixed(Instant.parse("2024-06-15T12:00:00Z"), ZoneId.of("GMT+2"));
@Override
protected TimeRange getRange(int start, int duration, ZoneId zoneId) { when(timeProviderMock.getInstantNow()).thenReturn(clock.instant());
return new TimeRange(1718402400000L, 1718488800000L); when(timeProviderMock.getZoneId()).thenReturn(clock.getZone());
when(timeProviderMock.getZonedDateTimeNow()).thenReturn(ZonedDateTime.now(clock));
AwattarBestPriceHandler handler = new AwattarBestPriceHandler(bestpriceMock, timeProviderMock) {
protected ZonedDateTime getStartTime(int start, ZoneId zoneId) {
return ZonedDateTime.of(2024, 6, 15, 12, 0, 0, 0, zoneId);
}
};
handler.setCallback(bestPriceCallbackMock);
ChannelUID channelUID = new ChannelUID(bestPriceUid, channelId);
handler.refreshChannel(channelUID);
verify(bestPriceCallbackMock).stateUpdated(channelUID, expectedState);
}
public static Stream<Arguments> testBestpriceHandlerChannels() {
return Stream.of( //
Arguments.of(12, 0, 24, 1, true, CHANNEL_HOURS, new StringType("14")),
Arguments.of(12, 0, 24, 1, true, CHANNEL_ACTIVE, OnOffType.from(false)),
Arguments.of(12, 0, 24, 1, true, CHANNEL_COUNTDOWN, new QuantityType<>("120 min")),
Arguments.of(12, 0, 24, 1, true, CHANNEL_REMAINING, new QuantityType<>("0 min")),
Arguments.of(13, 59, 24, 1, true, CHANNEL_COUNTDOWN, new QuantityType<>("1 min")),
Arguments.of(13, 59, 24, 1, true, CHANNEL_REMAINING, new QuantityType<>("0 min")),
Arguments.of(13, 59, 24, 1, false, CHANNEL_ACTIVE, OnOffType.from(false)),
Arguments.of(13, 59, 24, 1, true, CHANNEL_ACTIVE, OnOffType.from(false)),
Arguments.of(14, 01, 24, 1, true, CHANNEL_COUNTDOWN, new QuantityType<>("0 min")),
Arguments.of(14, 01, 24, 1, true, CHANNEL_REMAINING, new QuantityType<>("59 min")),
Arguments.of(14, 01, 24, 1, false, CHANNEL_ACTIVE, OnOffType.from(true)),
Arguments.of(14, 01, 24, 1, true, CHANNEL_ACTIVE, OnOffType.from(true)),
Arguments.of(14, 59, 24, 1, true, CHANNEL_COUNTDOWN, new QuantityType<>("0 min")),
Arguments.of(14, 59, 24, 1, true, CHANNEL_REMAINING, new QuantityType<>("1 min")),
Arguments.of(14, 59, 24, 1, false, CHANNEL_ACTIVE, OnOffType.from(true)),
Arguments.of(14, 59, 24, 1, true, CHANNEL_ACTIVE, OnOffType.from(true)),
Arguments.of(15, 00, 24, 1, true, CHANNEL_COUNTDOWN, new QuantityType<>("0 min")),
Arguments.of(15, 00, 24, 1, true, CHANNEL_REMAINING, new QuantityType<>("0 min")),
Arguments.of(15, 00, 24, 1, false, CHANNEL_ACTIVE, OnOffType.from(false)),
Arguments.of(15, 00, 24, 1, true, CHANNEL_ACTIVE, OnOffType.from(false)),
Arguments.of(12, 0, 24, 1, true, CHANNEL_REMAINING, new QuantityType<>("0 min")));
}
@ParameterizedTest
@MethodSource
void testBestpriceHandlerChannels(int currentHour, int currentMinute, int rangeDuration, int length,
boolean consecutive, String channelId, State expectedState) {
ThingUID bestPriceUid = new ThingUID(AwattarBindingConstants.THING_TYPE_BESTPRICE, "foo");
Map<String, Object> config = Map.of("rangeDuration", rangeDuration, "length", length, "consecutive",
consecutive);
when(bestpriceMock.getConfiguration()).thenReturn(new Configuration(config));
Clock clock = Clock.fixed(
ZonedDateTime.of(2024, 6, 15, currentHour, currentMinute, 0, 0, ZoneId.of("GMT+2")).toInstant(),
ZoneId.of("GMT+2"));
when(timeProviderMock.getInstantNow()).thenReturn(clock.instant());
when(timeProviderMock.getZoneId()).thenReturn(clock.getZone());
when(timeProviderMock.getZonedDateTimeNow())
.thenReturn(ZonedDateTime.ofInstant(clock.instant(), clock.getZone()));
AwattarBestPriceHandler handler = new AwattarBestPriceHandler(bestpriceMock, timeProviderMock) {
protected ZonedDateTime getStartTime(int start, ZoneId zoneId) {
return ZonedDateTime.of(2024, 6, 15, 0, 0, 0, 0, clock.getZone());
} }
}; };