diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceResult.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceResult.java index b14c4fe584aa5..ef9a866ca5f88 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceResult.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceResult.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.awattar.internal; +import java.time.Instant; + import org.eclipse.jdt.annotation.NonNullByDefault; /** @@ -24,7 +26,7 @@ public abstract class AwattarBestPriceResult { private long start; private long end; - public AwattarBestPriceResult() { + protected AwattarBestPriceResult() { } public long getStart() { @@ -47,7 +49,18 @@ public void updateEnd(long end) { } } - 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(); } diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java index 49e3d52126a9c..b648f2855084a 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarConsecutiveBestPriceResult.java @@ -76,8 +76,8 @@ public AwattarConsecutiveBestPriceResult(List prices, int length, } @Override - public boolean isActive() { - return contains(Instant.now().toEpochMilli()); + public boolean isActive(Instant pointInTime) { + return contains(pointInTime.toEpochMilli()); } public boolean contains(long timestamp) { diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java index 20d79272b581c..01ed9197c5860 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarNonConsecutiveBestPriceResult.java @@ -32,7 +32,6 @@ public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult { private final List members; private final ZoneId zoneId; - private boolean sorted = true; public AwattarNonConsecutiveBestPriceResult(List prices, int length, boolean inverted, ZoneId zoneId) { @@ -57,15 +56,14 @@ public AwattarNonConsecutiveBestPriceResult(List prices, int lengt } private void addMember(AwattarPrice member) { - sorted = false; members.add(member); updateStart(member.timerange().start()); updateEnd(member.timerange().end()); } @Override - public boolean isActive() { - return members.stream().anyMatch(x -> x.timerange().contains(Instant.now().toEpochMilli())); + public boolean isActive(Instant pointInTime) { + return members.stream().anyMatch(x -> x.timerange().contains(pointInTime.toEpochMilli())); } @Override @@ -73,16 +71,9 @@ public String toString() { return String.format("NonConsecutiveBestpriceResult with %s", members.toString()); } - private void sort() { - if (!sorted) { - members.sort(Comparator.comparingLong(p -> p.timerange().start())); - } - } - @Override public String getHours() { boolean second = false; - sort(); StringBuilder res = new StringBuilder(); for (AwattarPrice price : members) { diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarUtil.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarUtil.java index be9661d51b3cd..d4128e979d0cc 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarUtil.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarUtil.java @@ -20,7 +20,6 @@ import javax.measure.quantity.Time; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; @@ -32,9 +31,9 @@ @NonNullByDefault public class AwattarUtil { - public static long getMillisToNextMinute(int mod, TimeZoneProvider timeZoneProvider) { + public static long getMillisToNextMinute(int mod, ZoneId zoneId) { 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 offset = min % mod; offset = offset == 0 ? mod : offset; diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/api/AwattarApi.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/api/AwattarApi.java index b56454d293941..d2cfb91362cb5 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/api/AwattarApi.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/api/AwattarApi.java @@ -15,9 +15,8 @@ import static org.eclipse.jetty.http.HttpMethod.GET; import static org.eclipse.jetty.http.HttpStatus.OK_200; -import java.time.LocalDate; -import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.Comparator; import java.util.SortedSet; import java.util.TreeSet; @@ -31,6 +30,7 @@ import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration; import org.openhab.binding.awattar.internal.AwattarPrice; import org.openhab.binding.awattar.internal.dto.AwattarApiData; +import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider; import org.openhab.binding.awattar.internal.dto.Datum; import org.openhab.binding.awattar.internal.handler.TimeRange; import org.slf4j.Logger; @@ -58,7 +58,7 @@ public class AwattarApi { private double vatFactor; private double basePrice; - private ZoneId zone; + private AwattarTimeProvider timeProvider; private Gson gson; @@ -79,8 +79,8 @@ public AwattarApiException(String message) { * @param httpClient the HTTP client to use * @param zone the time zone to use */ - public AwattarApi(HttpClient httpClient, ZoneId zone, AwattarBridgeConfiguration config) { - this.zone = zone; + public AwattarApi(HttpClient httpClient, AwattarTimeProvider timeProvider, AwattarBridgeConfiguration config) { + this.timeProvider = timeProvider; this.httpClient = httpClient; this.gson = new Gson(); @@ -112,7 +112,7 @@ public AwattarApi(HttpClient httpClient, ZoneId zone, AwattarBridgeConfiguration public SortedSet getData() throws AwattarApiException { try { // we start one day in the past to cover ranges that already started yesterday - ZonedDateTime zdt = LocalDate.now(zone).atStartOfDay(zone).minusDays(1); + ZonedDateTime zdt = timeProvider.getZonedDateTimeNow().truncatedTo(ChronoUnit.DAYS).minusDays(1); long start = zdt.toInstant().toEpochMilli(); // Starting from midnight yesterday we add three days so that the range covers // the whole next day. diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/dto/AwattarTimeProvider.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/dto/AwattarTimeProvider.java new file mode 100644 index 0000000000000..084b924dd57ac --- /dev/null +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/dto/AwattarTimeProvider.java @@ -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()); + } +} diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestPriceHandler.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestPriceHandler.java index c166a51cc85bb..9de71f8c32e11 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestPriceHandler.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestPriceHandler.java @@ -39,7 +39,7 @@ import org.openhab.binding.awattar.internal.AwattarConsecutiveBestPriceResult; import org.openhab.binding.awattar.internal.AwattarNonConsecutiveBestPriceResult; 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.OnOffType; 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 final Logger logger = LoggerFactory.getLogger(AwattarBestPriceHandler.class); + private final AwattarTimeProvider timeProvider; private @Nullable ScheduledFuture thingRefresher; - private final TimeZoneProvider timeZoneProvider; - - public AwattarBestPriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) { + public AwattarBestPriceHandler(Thing thing, AwattarTimeProvider timeProvider) { super(thing); - this.timeZoneProvider = timeZoneProvider; + this.timeProvider = timeProvider; } @Override @@ -97,7 +96,7 @@ public void initialize() { * here */ thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels, - getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000L, + getMillisToNextMinute(1, timeProvider.getZoneId()), THING_REFRESH_INTERVAL * 1000L, TimeUnit.MILLISECONDS); } } @@ -141,7 +140,7 @@ public void refreshChannel(ChannelUID channelUID) { return; } - ZoneId zoneId = bridgeHandler.getTimeZone(); + ZoneId zoneId = timeProvider.getZoneId(); AwattarBestPriceConfiguration config = getConfigAs(AwattarBestPriceConfiguration.class); TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, zoneId); @@ -163,7 +162,7 @@ public void refreshChannel(ChannelUID channelUID) { long diff; switch (channelId) { case CHANNEL_ACTIVE: - state = OnOffType.from(result.isActive()); + state = OnOffType.from(result.isActive(timeProvider.getInstantNow())); break; case CHANNEL_START: state = new DateTimeType(Instant.ofEpochMilli(result.getStart())); @@ -172,7 +171,7 @@ public void refreshChannel(ChannelUID channelUID) { state = new DateTimeType(Instant.ofEpochMilli(result.getEnd())); break; case CHANNEL_COUNTDOWN: - diff = result.getStart() - Instant.now().toEpochMilli(); + diff = result.getStart() - timeProvider.getInstantNow().toEpochMilli(); if (diff >= 0) { state = getDuration(diff); } else { @@ -180,8 +179,8 @@ public void refreshChannel(ChannelUID channelUID) { } break; case CHANNEL_REMAINING: - if (result.isActive()) { - diff = result.getEnd() - Instant.now().toEpochMilli(); + if (result.isActive(timeProvider.getInstantNow())) { + diff = result.getEnd() - timeProvider.getInstantNow().toEpochMilli(); state = getDuration(diff); } else { state = QuantityType.valueOf(0, Units.MINUTE); @@ -216,20 +215,39 @@ private List getPriceRange(AwattarBridgeHandler bridgeHandler, Tim 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) { - ZonedDateTime startCal = getCalendarForHour(start, zoneId); - ZonedDateTime endCal = startCal.plusHours(duration); - ZonedDateTime now = ZonedDateTime.now(zoneId); + ZonedDateTime startTime = getStartTime(start, zoneId); + ZonedDateTime endTime = startTime.plusHours(duration); + ZonedDateTime now = timeProvider.getZonedDateTimeNow(); if (now.getHour() < start) { // we are before the range, so we might be still within the last range - startCal = startCal.minusDays(1); - endCal = endCal.minusDays(1); + startTime = startTime.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 - startCal = startCal.plusDays(1); - endCal = endCal.plusDays(1); + startTime = startTime.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); } } diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandler.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandler.java index db5eee87cdedc..324a758dffc52 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandler.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandler.java @@ -16,12 +16,11 @@ import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_TOTAL_NET; import java.time.Instant; -import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.SortedSet; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.function.Function; +import java.util.function.ToDoubleFunction; import javax.measure.Unit; @@ -32,7 +31,7 @@ import org.openhab.binding.awattar.internal.AwattarPrice; import org.openhab.binding.awattar.internal.api.AwattarApi; import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException; -import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.CurrencyUnits; 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 HttpClient httpClient; + private final AwattarTimeProvider timeProvider; private @Nullable ScheduledFuture dataRefresher; private Instant lastRefresh = Instant.EPOCH; // This cache stores price data for up to two days private @Nullable SortedSet prices; - private ZoneId zone; private @Nullable AwattarApi awattarApi; - public AwattarBridgeHandler(Bridge thing, HttpClient httpClient, TimeZoneProvider timeZoneProvider) { + public AwattarBridgeHandler(Bridge thing, HttpClient httpClient, AwattarTimeProvider timeProvider) { super(thing); this.httpClient = httpClient; - zone = timeZoneProvider.getTimeZone(); + this.timeProvider = timeProvider; } @Override @@ -87,7 +86,7 @@ public void initialize() { AwattarBridgeConfiguration config = getConfigAs(AwattarBridgeConfiguration.class); try { - awattarApi = new AwattarApi(httpClient, zone, config); + awattarApi = new AwattarApi(httpClient, timeProvider, config); dataRefresher = scheduler.scheduleWithFixedDelay(this::refreshIfNeeded, 0, DATA_REFRESH_INTERVAL * 1000L, TimeUnit.MILLISECONDS); @@ -154,17 +153,15 @@ private Unit getPriceUnit() { return priceUnit; } - private void createAndSendTimeSeries(String channelId, Function valueFunction) { + private void createAndSendTimeSeries(String channelId, ToDoubleFunction valueFunction) { SortedSet locPrices = getPrices(); Unit priceUnit = getPriceUnit(); if (locPrices == null) { return; } TimeSeries timeSeries = new TimeSeries(TimeSeries.Policy.REPLACE); - locPrices.forEach(p -> { - timeSeries.add(Instant.ofEpochMilli(p.timerange().start()), - new QuantityType<>(valueFunction.apply(p) / 100.0, priceUnit)); - }); + locPrices.forEach(p -> timeSeries.add(Instant.ofEpochMilli(p.timerange().start()), + new QuantityType<>(valueFunction.applyAsDouble(p) / 100.0, priceUnit))); sendTimeSeries(channelId, timeSeries); } @@ -186,11 +183,13 @@ private void createAndSendTimeSeries(String channelId, Function getPrices() { if (prices == null) { @@ -261,6 +256,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_MARKET_NET -> createAndSendTimeSeries(CHANNEL_MARKET_NET, AwattarPrice::netPrice); case CHANNEL_TOTAL_NET -> createAndSendTimeSeries(CHANNEL_TOTAL_NET, AwattarPrice::netTotal); + default -> logger.warn("Channel {} not supported", channelUID.getId()); } } } diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java index ec5fac0aa119a..94502e60d52ea 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java @@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; @@ -50,13 +51,13 @@ public class AwattarHandlerFactory extends BaseThingHandlerFactory { private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PRICE, THING_TYPE_BESTPRICE, THING_TYPE_BRIDGE); private final HttpClient httpClient; - private final TimeZoneProvider timeZoneProvider; + private final AwattarTimeProvider timeProvider; @Activate public AwattarHandlerFactory(final @Reference HttpClientFactory httpClientFactory, final @Reference TimeZoneProvider timeZoneProvider) { this.httpClient = httpClientFactory.getCommonHttpClient(); - this.timeZoneProvider = timeZoneProvider; + this.timeProvider = new AwattarTimeProvider(timeZoneProvider); } @Override @@ -69,11 +70,11 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); 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)) { - return new AwattarPriceHandler(thing, timeZoneProvider); + return new AwattarPriceHandler(thing, timeProvider); } 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); diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarPriceHandler.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarPriceHandler.java index 8f68fbf114243..0ddf76aad19ca 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarPriceHandler.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarPriceHandler.java @@ -30,7 +30,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; 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.thing.Bridge; import org.openhab.core.thing.Channel; @@ -55,15 +55,16 @@ */ @NonNullByDefault public class AwattarPriceHandler extends BaseThingHandler { + private final AwattarTimeProvider timeProvider; + private static final int THING_REFRESH_INTERVAL = 60; private final Logger logger = LoggerFactory.getLogger(AwattarPriceHandler.class); - private final TimeZoneProvider timeZoneProvider; private @Nullable ScheduledFuture thingRefresher; - public AwattarPriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) { + public AwattarPriceHandler(Thing thing, AwattarTimeProvider timeProvider) { super(thing); - this.timeZoneProvider = timeZoneProvider; + this.timeProvider = timeProvider; } @Override @@ -90,7 +91,7 @@ public void initialize() { * here */ thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels, - getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000, + getMillisToNextMinute(1, timeProvider.getZoneId()), THING_REFRESH_INTERVAL * 1000L, TimeUnit.MILLISECONDS); } } @@ -139,11 +140,11 @@ public void refreshChannel(ChannelUID channelUID) { ZonedDateTime target; if (group.equals(CHANNEL_GROUP_CURRENT)) { - target = ZonedDateTime.now(bridgeHandler.getTimeZone()); + target = timeProvider.getZonedDateTimeNow(); } 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")) { - target = getCalendarForHour(Integer.parseInt(group.substring(8)), bridgeHandler.getTimeZone()).plusDays(1); + target = getCalendarForHour(Integer.parseInt(group.substring(8)), timeProvider.getZoneId()).plusDays(1); } else { logger.warn("Unsupported channel group {}", group); updateState(channelUID, state); @@ -153,7 +154,7 @@ public void refreshChannel(ChannelUID channelUID) { AwattarPrice price = bridgeHandler.getPriceFor(target.toInstant().toEpochMilli()); if (price == null) { - logger.trace("No price found for hour {}", target.toString()); + logger.trace("No price found for hour {}", target); updateState(channelUID, state); return; } diff --git a/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/AwattarBestPriceTest.java b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/AwattarBestPriceTest.java index c7a20a28590ee..10edf635ddec1 100644 --- a/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/AwattarBestPriceTest.java +++ b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/AwattarBestPriceTest.java @@ -24,6 +24,7 @@ import java.util.SortedSet; import java.util.TreeSet; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; import org.openhab.binding.awattar.internal.handler.TimeRange; @@ -33,6 +34,7 @@ * * @author Thomas Leber - Initial contribution */ +@NonNullByDefault public class AwattarBestPriceTest { private ZoneId zoneId = ZoneId.of("GMT"); @@ -74,7 +76,7 @@ public synchronized SortedSet getPrices() { } @Test - void AwattarConsecutiveBestPriceResult() { + void awattarConsecutiveBestPriceResult() { int length = 8; List range = new ArrayList<>(getPrices()); @@ -85,7 +87,7 @@ void AwattarConsecutiveBestPriceResult() { } @Test - void AwattarNonConsecutiveBestPriceResult_nonInverted() { + void awattarNonConsecutiveBestPriceResultNonInverted() { int length = 6; boolean inverted = false; @@ -98,7 +100,7 @@ void AwattarNonConsecutiveBestPriceResult_nonInverted() { } @Test - void AwattarNonConsecutiveBestPriceResult_inverted() { + void awattarNonConsecutiveBestPriceResultInverted() { int length = 4; boolean inverted = true; diff --git a/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/api/AwattarApiTest.java b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/api/AwattarApiTest.java index 615317cf6f351..eb8d19f0129d5 100644 --- a/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/api/AwattarApiTest.java +++ b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/api/AwattarApiTest.java @@ -17,12 +17,16 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Objects; import java.util.SortedSet; import java.util.concurrent.ExecutionException; @@ -45,7 +49,7 @@ import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration; import org.openhab.binding.awattar.internal.AwattarPrice; import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException; -import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider; import org.openhab.core.test.java.JavaTest; /** @@ -60,10 +64,10 @@ class AwattarApiTest extends JavaTest { // API Mocks private @Mock @NonNullByDefault({}) HttpClient httpClientMock; - private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock; private @Mock @NonNullByDefault({}) Request requestMock; private @Mock @NonNullByDefault({}) ContentResponse contentResponseMock; private @Mock @NonNullByDefault({}) AwattarBridgeConfiguration config; + private @Mock @NonNullByDefault({}) AwattarTimeProvider timeProviderMock; // sut private @NonNullByDefault({}) AwattarApi api; @@ -86,30 +90,33 @@ public void setUp() throws IOException, ExecutionException, InterruptedException when(requestMock.timeout(10, TimeUnit.SECONDS)).thenReturn(requestMock); 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.vatPercent = 0.0; config.country = "DE"; - api = new AwattarApi(httpClientMock, ZoneId.of("GMT+2"), config); + api = new AwattarApi(httpClientMock, timeProviderMock, config); } @Test void testDeUrl() throws AwattarApiException { 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 void testAtUrl() throws AwattarApiException { config.country = "AT"; - api = new AwattarApi(httpClientMock, ZoneId.of("GMT+2"), config); + api = new AwattarApi(httpClientMock, timeProviderMock, config); 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 @@ -117,7 +124,7 @@ void testInvalidCountry() { config.country = "CH"; 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'")); } diff --git a/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerRefreshTest.java b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerRefreshTest.java index e2d34a095bd16..166141ac798a5 100644 --- a/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerRefreshTest.java +++ b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerRefreshTest.java @@ -19,14 +19,20 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.time.Clock; +import java.time.Instant; import java.time.ZoneId; import java.util.List; +import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jetty.client.HttpClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.platform.commons.support.HierarchyTraversalMode; import org.junit.platform.commons.support.ReflectionSupport; import org.mockito.Mock; @@ -36,7 +42,7 @@ import org.openhab.binding.awattar.internal.AwattarBindingConstants; import org.openhab.binding.awattar.internal.api.AwattarApi; import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException; -import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider; import org.openhab.core.test.java.JavaTest; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; @@ -62,8 +68,9 @@ class AwattarBridgeHandlerRefreshTest extends JavaTest { private @Mock @NonNullByDefault({}) Bridge bridgeMock; private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock; private @Mock @NonNullByDefault({}) HttpClient httpClientMock; - private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock; + private @Mock @NonNullByDefault({}) Clock fixedClock; private @Mock @NonNullByDefault({}) AwattarApi awattarApiMock; + private @Mock @NonNullByDefault({}) AwattarTimeProvider timeProviderMock; // best price handler mocks private @Mock @NonNullByDefault({}) Thing bestpriceMock; @@ -73,10 +80,11 @@ class AwattarBridgeHandlerRefreshTest extends JavaTest { @BeforeEach public void setUp() throws IllegalArgumentException, IllegalAccessException { - when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of("GMT+2")); - 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); List fields = ReflectionSupport.findFields(AwattarBridgeHandler.class, @@ -95,7 +103,7 @@ public void setUp() throws IllegalArgumentException, IllegalAccessException { * @throws AwattarApiException */ @Test - void testRefreshIfNeeded_ThingOffline() throws SecurityException, AwattarApiException { + void testRefreshIfNeededThingOffline() throws SecurityException, AwattarApiException { when(bridgeMock.getStatus()).thenReturn(ThingStatus.OFFLINE); bridgeHandler.refreshIfNeeded(); @@ -113,7 +121,7 @@ void testRefreshIfNeeded_ThingOffline() throws SecurityException, AwattarApiExce * @throws AwattarApiException */ @Test - void testRefreshIfNeeded_DataEmpty() throws SecurityException, AwattarApiException { + void testRefreshIfNeededDataEmpty() throws SecurityException, AwattarApiException { when(bridgeMock.getStatus()).thenReturn(ThingStatus.ONLINE); bridgeHandler.refreshIfNeeded(); @@ -124,7 +132,7 @@ void testRefreshIfNeeded_DataEmpty() throws SecurityException, AwattarApiExcepti } @Test - void testNeedRefresh_ThingOffline() throws SecurityException { + void testNeedRefreshThingOffline() throws SecurityException { when(bridgeMock.getStatus()).thenReturn(ThingStatus.OFFLINE); // get private method via reflection @@ -136,7 +144,7 @@ void testNeedRefresh_ThingOffline() throws SecurityException { } @Test - void testNeedRefresh_DataEmpty() throws SecurityException, IllegalArgumentException, IllegalAccessException { + void testNeedRefreshDataEmpty() throws SecurityException, IllegalArgumentException, IllegalAccessException { when(bridgeMock.getStatus()).thenReturn(ThingStatus.ONLINE); List fields = ReflectionSupport.findFields(AwattarBridgeHandler.class, @@ -154,4 +162,68 @@ void testNeedRefresh_DataEmpty() throws SecurityException, IllegalArgumentExcept assertThat(result, is(true)); } + + public static Stream 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)); + } } diff --git a/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerTest.java b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerTest.java index 2a7999b83f8c6..a9fc5396be966 100644 --- a/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerTest.java +++ b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerTest.java @@ -20,14 +20,20 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_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_HOURS; +import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_REMAINING; import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_START; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; +import java.time.Clock; +import java.time.Instant; import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -55,9 +61,11 @@ import org.openhab.binding.awattar.internal.api.AwattarApi; import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException; import org.openhab.binding.awattar.internal.dto.AwattarApiData; +import org.openhab.binding.awattar.internal.dto.AwattarTimeProvider; 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.OnOffType; +import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; import org.openhab.core.test.java.JavaTest; import org.openhab.core.thing.Bridge; @@ -84,8 +92,8 @@ public class AwattarBridgeHandlerTest extends JavaTest { private @Mock @NonNullByDefault({}) Bridge bridgeMock; private @Mock @NonNullByDefault({}) ThingHandlerCallback bridgeCallbackMock; private @Mock @NonNullByDefault({}) HttpClient httpClientMock; - private @Mock @NonNullByDefault({}) TimeZoneProvider timeZoneProviderMock; private @Mock @NonNullByDefault({}) AwattarApi awattarApiMock; + private @Mock @NonNullByDefault({}) AwattarTimeProvider timeProviderMock; // best price handler mocks private @Mock @NonNullByDefault({}) Thing bestpriceMock; @@ -109,10 +117,8 @@ public void setUp() throws IOException, IllegalArgumentException, IllegalAccessE when(awattarApiMock.getData()).thenReturn(result); } - when(timeZoneProviderMock.getTimeZone()).thenReturn(ZoneId.of("GMT+2")); - when(bridgeMock.getUID()).thenReturn(BRIDGE_UID); - bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeZoneProviderMock); + bridgeHandler = new AwattarBridgeHandler(bridgeMock, httpClientMock, timeProviderMock); bridgeHandler.setCallback(bridgeCallbackMock); // mock the private field awattarApi @@ -166,31 +172,105 @@ void testContainsPriceForRange() { public static Stream 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"))); + Arguments.of(24, 1, true, CHANNEL_START, new DateTimeType("2024-06-15T14:00:00.000+0200")), + Arguments.of(24, 1, true, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")), + Arguments.of(24, 1, true, CHANNEL_HOURS, new StringType("14")), + Arguments.of(24, 1, false, CHANNEL_START, new DateTimeType("2024-06-15T14:00:00.000+0200")), + Arguments.of(24, 1, false, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")), + Arguments.of(24, 1, false, CHANNEL_HOURS, new StringType("14")), + Arguments.of(24, 2, true, CHANNEL_START, new DateTimeType("2024-06-15T13:00:00.000+0200")), + Arguments.of(24, 2, true, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")), + Arguments.of(24, 2, true, CHANNEL_HOURS, new StringType("13,14")), + Arguments.of(24, 2, false, CHANNEL_START, new DateTimeType("2024-06-15T13:00:00.000+0200")), + Arguments.of(24, 2, false, CHANNEL_END, new DateTimeType("2024-06-15T15:00:00.000+0200")), + 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 + @MethodSource + void testBestpriceHandler(int rangeDuration, int length, boolean consecutive, String channelId, + State expectedState) { + ThingUID bestPriceUid = new ThingUID(AwattarBindingConstants.THING_TYPE_BESTPRICE, "foo"); + Map config = Map.of("rangeDuration", rangeDuration, "length", length, "consecutive", + consecutive); + when(bestpriceMock.getConfiguration()).thenReturn(new Configuration(config)); + + Clock clock = Clock.fixed(Instant.parse("2024-06-15T12:00:00Z"), ZoneId.of("GMT+2")); + + when(timeProviderMock.getInstantNow()).thenReturn(clock.instant()); + 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 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 testBestpriceHandler(int length, boolean consecutive, String channelId, State expectedState) { + 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 config = Map.of("length", length, "consecutive", consecutive); + Map config = Map.of("rangeDuration", rangeDuration, "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); + 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()); } };