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 1318348085b..9aa80dd26d1 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 @@ -17,6 +17,7 @@ import static org.openhab.binding.awattar.internal.AwattarUtil.getHourFrom; import java.time.Instant; import java.time.ZoneId; +import java.util.Comparator; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -33,23 +34,45 @@ public class AwattarConsecutiveBestPriceResult extends AwattarBestPriceResult { private final String hours; private final ZoneId zoneId; - public AwattarConsecutiveBestPriceResult(List prices, ZoneId zoneId) { + public AwattarConsecutiveBestPriceResult(List prices, int length, ZoneId zoneId) { super(); this.zoneId = zoneId; - StringBuilder hours = new StringBuilder(); - boolean second = false; - for (AwattarPrice price : prices) { + + // sort the prices by timerange + prices.sort(Comparator.comparing(AwattarPrice::timerange)); + + // calculate the range with the lowest accumulated price of length hours from the given prices + double minPrice = Double.MAX_VALUE; + int minIndex = 0; + for (int i = 0; i <= prices.size() - length; i++) { + double sum = 0; + for (int j = 0; j < length; j++) { + sum += prices.get(i + j).netPrice(); + } + if (sum < minPrice) { + minPrice = sum; + minIndex = i; + } + } + + // calculate the accumulated price and the range of the best price + for (int i = 0; i < length; i++) { + AwattarPrice price = prices.get(minIndex + i); priceSum += price.netPrice(); - length++; updateStart(price.timerange().start()); updateEnd(price.timerange().end()); - if (second) { - hours.append(','); - } - hours.append(getHourFrom(price.timerange().start(), zoneId)); - second = true; } - this.hours = hours.toString(); + + // create a list of hours for the best price range + StringBuilder locHours = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i > 0) { + locHours.append(","); + } + locHours.append(getHourFrom(prices.get(minIndex + i).timerange().start(), zoneId)); + } + + this.hours = locHours.toString(); } @Override @@ -61,10 +84,6 @@ public class AwattarConsecutiveBestPriceResult extends AwattarBestPriceResult { return timestamp >= getStart() && timestamp < getEnd(); } - public double getPriceSum() { - return priceSum; - } - @Override public String toString() { return String.format("{%s, %s, %.2f}", formatDate(getStart(), zoneId), formatDate(getEnd(), zoneId), 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 bf623d24c17..461292b66f7 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 @@ -17,6 +17,7 @@ import static org.openhab.binding.awattar.internal.AwattarUtil.*; import java.time.Instant; import java.time.ZoneId; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -33,13 +34,29 @@ public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult private final ZoneId zoneId; private boolean sorted = true; - public AwattarNonConsecutiveBestPriceResult(ZoneId zoneId) { + public AwattarNonConsecutiveBestPriceResult(List prices, int length, boolean inverted, + ZoneId zoneId) { super(); this.zoneId = zoneId; members = new ArrayList<>(); + + prices.sort(Comparator.naturalOrder()); + + // sort in descending order when inverted + if (inverted) { + Collections.reverse(prices); + } + + // take up to config.length prices + for (int i = 0; i < Math.min(length, prices.size()); i++) { + addMember(prices.get(i)); + } + + // sort the members + members.sort(Comparator.comparing(AwattarPrice::timerange)); } - public void addMember(AwattarPrice member) { + private void addMember(AwattarPrice member) { sorted = false; members.add(member); updateStart(member.timerange().start()); @@ -67,6 +84,7 @@ public class AwattarNonConsecutiveBestPriceResult extends AwattarBestPriceResult boolean second = false; sort(); StringBuilder res = new StringBuilder(); + for (AwattarPrice price : members) { if (second) { res.append(','); 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 39540ae5de4..9c7e3de1883 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 @@ -28,8 +28,6 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.SortedSet; import java.util.concurrent.ScheduledFuture; @@ -128,11 +126,13 @@ public class AwattarBestPriceHandler extends BaseThingHandler { public void refreshChannel(ChannelUID channelUID) { State state = UnDefType.UNDEF; Bridge bridge = getBridge(); + if (bridge == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.bridge.missing"); updateState(channelUID, state); return; } + AwattarBridgeHandler bridgeHandler = (AwattarBridgeHandler) bridge.getHandler(); if (bridgeHandler == null || bridgeHandler.getPrices() == null) { logger.debug("No prices available, so can't refresh channel."); @@ -140,8 +140,11 @@ public class AwattarBestPriceHandler extends BaseThingHandler { updateState(channelUID, state); return; } + + ZoneId zoneId = bridgeHandler.getTimeZone(); + AwattarBestPriceConfiguration config = getConfigAs(AwattarBestPriceConfiguration.class); - TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, bridgeHandler.getTimeZone()); + TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, zoneId); if (!(bridgeHandler.containsPriceFor(timerange))) { updateState(channelUID, state); return; @@ -151,36 +154,11 @@ public class AwattarBestPriceHandler extends BaseThingHandler { List range = getPriceRange(bridgeHandler, timerange); if (config.consecutive) { - range.sort(Comparator.comparing(AwattarPrice::timerange)); - AwattarConsecutiveBestPriceResult res = new AwattarConsecutiveBestPriceResult( - range.subList(0, config.length), bridgeHandler.getTimeZone()); - - for (int i = 1; i <= range.size() - config.length; i++) { - AwattarConsecutiveBestPriceResult res2 = new AwattarConsecutiveBestPriceResult( - range.subList(i, i + config.length), bridgeHandler.getTimeZone()); - if (res2.getPriceSum() < res.getPriceSum()) { - res = res2; - } - } - result = res; + result = new AwattarConsecutiveBestPriceResult(range, config.length, zoneId); } else { - range.sort(Comparator.naturalOrder()); - - // sort in descending order when inverted - if (config.inverted) { - Collections.reverse(range); - } - - AwattarNonConsecutiveBestPriceResult res = new AwattarNonConsecutiveBestPriceResult( - bridgeHandler.getTimeZone()); - - // take up to config.length prices - for (int i = 0; i < Math.min(config.length, range.size()); i++) { - res.addMember(range.get(i)); - } - - result = res; + result = new AwattarNonConsecutiveBestPriceResult(range, config.length, config.inverted, zoneId); } + String channelId = channelUID.getIdWithoutGroup(); long diff; switch (channelId) { 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 new file mode 100644 index 00000000000..b180f11482a --- /dev/null +++ b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/AwattarBestPriceTest.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.binding.awattar.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import org.junit.jupiter.api.Test; +import org.openhab.binding.awattar.internal.handler.TimeRange; + +/** + * The {@link AwattarBestPriceTest} contains tests for the + * {@link AwattarConsecutiveBestPriceResult} and {@link AwattarNonConsecutiveBestPriceResult} logic. + * + * @author Thomas Leber - Initial contribution + */ +public class AwattarBestPriceTest { + + private ZoneId zoneId = ZoneId.of("GMT"); + + public static ZonedDateTime getCalendarForHour(int hour, ZoneId zone) { + return ZonedDateTime.ofInstant(Instant.ofEpochSecond(1731283200L), zone).truncatedTo(ChronoUnit.HOURS) + .plusHours(hour); + } + + public synchronized SortedSet getPrices() { + SortedSet prices = new TreeSet<>(Comparator.comparing(AwattarPrice::timerange)); + + prices.add(new AwattarPrice(103.87, 103.87, 103.87, 103.87, new TimeRange(1731283200000L, 1731286800000L))); + prices.add(new AwattarPrice(100.06, 100.06, 100.06, 100.06, new TimeRange(1731286800000L, 1731290400000L))); + prices.add(new AwattarPrice(99.06, 99.06, 99.06, 99.06, new TimeRange(1731290400000L, 1731294000000L))); + prices.add(new AwattarPrice(99.12, 99.12, 99.12, 99.12, new TimeRange(1731294000000L, 1731297600000L))); + prices.add(new AwattarPrice(105.16, 105.16, 105.16, 105.16, new TimeRange(1731297600000L, 1731301200000L))); + prices.add(new AwattarPrice(124.96, 124.96, 124.96, 124.96, new TimeRange(1731301200000L, 1731304800000L))); + prices.add(new AwattarPrice(143.91, 143.91, 143.91, 143.91, new TimeRange(1731304800000L, 1731308400000L))); + prices.add(new AwattarPrice(141.95, 141.95, 141.95, 141.95, new TimeRange(1731308400000L, 1731312000000L))); + prices.add(new AwattarPrice(135.95, 135.95, 135.95, 135.95, new TimeRange(1731312000000L, 1731315600000L))); + prices.add(new AwattarPrice(130.39, 130.39, 130.39, 130.39, new TimeRange(1731315600000L, 1731319200000L))); + prices.add(new AwattarPrice(124.5, 124.5, 124.5, 124.5, new TimeRange(1731319200000L, 1731322800000L))); + prices.add(new AwattarPrice(119.79, 119.79, 119.79, 119.79, new TimeRange(1731322800000L, 1731326400000L))); + prices.add(new AwattarPrice(131.13, 131.13, 131.13, 131.13, new TimeRange(1731326400000L, 1731330000000L))); + prices.add(new AwattarPrice(133.72, 133.72, 133.72, 133.72, new TimeRange(1731330000000L, 1731333600000L))); + prices.add(new AwattarPrice(141.58, 141.58, 141.58, 141.58, new TimeRange(1731333600000L, 1731337200000L))); + prices.add(new AwattarPrice(146.94, 146.94, 146.94, 146.94, new TimeRange(1731337200000L, 1731340800000L))); + prices.add(new AwattarPrice(150.08, 150.08, 150.08, 150.08, new TimeRange(1731340800000L, 1731344400000L))); + prices.add(new AwattarPrice(146.9, 146.9, 146.9, 146.9, new TimeRange(1731344400000L, 1731348000000L))); + prices.add(new AwattarPrice(139.87, 139.87, 139.87, 139.87, new TimeRange(1731348000000L, 1731351600000L))); + prices.add(new AwattarPrice(123.78, 123.78, 123.78, 123.78, new TimeRange(1731351600000L, 1731355200000L))); + prices.add(new AwattarPrice(119.02, 119.02, 119.02, 119.02, new TimeRange(1731355200000L, 1731358800000L))); + prices.add(new AwattarPrice(116.87, 116.87, 116.87, 116.87, new TimeRange(1731358800000L, 1731362400000L))); + prices.add(new AwattarPrice(109.72, 109.72, 109.72, 109.72, new TimeRange(1731362400000L, 1731366000000L))); + prices.add(new AwattarPrice(107.89, 107.89, 107.89, 107.89, new TimeRange(1731366000000L, 1731369600000L))); + + return prices; + } + + @Test + void AwattarConsecutiveBestPriceResult() { + int length = 8; + + List range = new ArrayList<>(getPrices()); + + range.sort(Comparator.comparing(AwattarPrice::timerange)); + AwattarConsecutiveBestPriceResult result = new AwattarConsecutiveBestPriceResult(range, length, zoneId); + assertEquals("00,01,02,03,04,05,06,07", result.getHours()); + } + + @Test + void AwattarNonConsecutiveBestPriceResult_nonInverted() { + int length = 6; + boolean inverted = false; + + List range = new ArrayList<>(getPrices()); + + range.sort(Comparator.comparing(AwattarPrice::timerange)); + AwattarNonConsecutiveBestPriceResult result = new AwattarNonConsecutiveBestPriceResult(range, length, inverted, + zoneId); + assertEquals("00,01,02,03,04,23", result.getHours()); + } + + @Test + void AwattarNonConsecutiveBestPriceResult_inverted() { + int length = 4; + boolean inverted = true; + + List range = new ArrayList<>(getPrices()); + + range.sort(Comparator.comparing(AwattarPrice::timerange)); + AwattarNonConsecutiveBestPriceResult result = new AwattarNonConsecutiveBestPriceResult(range, length, inverted, + zoneId); + assertEquals("06,15,16,17", result.getHours()); + } +} 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 f543757746d..3d7d232f4d0 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 @@ -45,8 +45,6 @@ import org.mockito.quality.Strictness; import org.openhab.binding.awattar.internal.AwattarBridgeConfiguration; import org.openhab.binding.awattar.internal.AwattarPrice; import org.openhab.binding.awattar.internal.api.AwattarApi.AwattarApiException; -import org.openhab.binding.awattar.internal.handler.AwattarBridgeHandler; -import org.openhab.binding.awattar.internal.handler.AwattarBridgeHandlerTest; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.test.java.JavaTest;