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 b949fbaa099f4..88942156fa1d1 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; /** @@ -47,7 +49,18 @@ public void updateEnd(long end) { } } - public abstract boolean isActive(); + /** + * Returns true if the best price is active. + * + * @param now the current time + * @return true if the best price is active, false otherwise + */ + public abstract boolean isActive(Instant now); + /** + * 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 9aa80dd26d188..0696cbb26890b 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 now) { + return contains(now.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 461292b66f7e8..ef9e56c9e306f 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 @@ -64,8 +64,8 @@ private void addMember(AwattarPrice member) { } @Override - public boolean isActive() { - return members.stream().anyMatch(x -> x.timerange().contains(Instant.now().toEpochMilli())); + public boolean isActive(Instant now) { + return members.stream().anyMatch(x -> x.timerange().contains(now.toEpochMilli())); } @Override @@ -73,16 +73,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/handler/AwattarBestPriceHandler.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestPriceHandler.java index 9c7e3de1883e7..5d57725c9b0c6 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 @@ -24,7 +24,6 @@ import static org.openhab.binding.awattar.internal.AwattarUtil.getDuration; import static org.openhab.binding.awattar.internal.AwattarUtil.getMillisToNextMinute; -import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -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(getNow(zoneId).toInstant())); break; case CHANNEL_START: state = getDateTimeType(result.getStart(), timeZoneProvider); @@ -172,7 +171,7 @@ public void refreshChannel(ChannelUID channelUID) { state = getDateTimeType(result.getEnd(), timeZoneProvider); break; case CHANNEL_COUNTDOWN: - diff = result.getStart() - Instant.now().toEpochMilli(); + diff = result.getStart() - getNow(zoneId).toInstant().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(getNow(zoneId).toInstant())) { + diff = result.getEnd() - getNow(zoneId).toInstant().toEpochMilli(); state = getDuration(diff); } else { state = QuantityType.valueOf(0, Units.MINUTE); @@ -216,20 +215,49 @@ 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 = getStarTime(start, zoneId); + ZonedDateTime endTime = startTime.plusHours(duration); + ZonedDateTime now = getNow(zoneId); 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.toInstant().toEpochMilli() < now.toInstant().toEpochMilli()) { // 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 getStarTime(int start, ZoneId zoneId) { + return getCalendarForHour(start, zoneId); + } + + /** + * Returns the current time. + * + * @param zoneId the time zone + * @return the current time + */ + protected ZonedDateTime getNow(ZoneId zoneId) { + return ZonedDateTime.now(zoneId); } } 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 37dde69ac6497..a846e07b46272 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,18 @@ 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.ZoneId; +import java.time.ZonedDateTime; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -58,6 +62,8 @@ 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; @@ -166,31 +172,98 @@ 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 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"); - 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); + protected ZonedDateTime getStarTime(int start, ZoneId zoneId) { + return ZonedDateTime.of(2024, 6, 15, 12, 0, 0, 0, zoneId); + } + + protected ZonedDateTime getNow(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 testBestpriceHandler_channels() { + 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_channels(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("rangeDuration", rangeDuration, "length", length, "consecutive", + consecutive); + when(bestpriceMock.getConfiguration()).thenReturn(new Configuration(config)); + + AwattarBestPriceHandler handler = new AwattarBestPriceHandler(bestpriceMock, timeZoneProviderMock) { + protected ZonedDateTime getStarTime(int start, ZoneId zoneId) { + return ZonedDateTime.of(2024, 6, 15, 0, 0, 0, 0, zoneId); + } + + protected ZonedDateTime getNow(ZoneId zoneId) { + return ZonedDateTime.of(2024, 6, 15, currentHour, currentMinute, 0, 0, zoneId); } };