From 221c9dd7564b339fee55729547a542a952b1de33 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Wed, 13 Nov 2024 07:41:17 +0100 Subject: [PATCH] Fix last hour not being considered for best price (#17731) Resolves #17316 Signed-off-by: Jacob Laursen Signed-off-by: Thomas Leber --- .../AwattarConsecutiveBestPriceResult.java | 47 ++++--- .../AwattarNonConsecutiveBestPriceResult.java | 22 +++- .../handler/AwattarBestPriceHandler.java | 43 ++----- .../handler/AwattarBridgeHandler.java | 16 ++- .../awattar/internal/handler/TimeRange.java | 1 + .../internal/AwattarBestPriceTest.java | 119 ++++++++++++++++++ .../handler/AwattarBridgeHandlerTest.java | 10 +- 7 files changed, 207 insertions(+), 51 deletions(-) create mode 100644 bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/AwattarBestPriceTest.java 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 1318348085b65..9aa80dd26d188 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 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(','); + } + + // 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(","); } - hours.append(getHourFrom(price.timerange().start(), zoneId)); - second = true; + locHours.append(getHourFrom(prices.get(minIndex + i).timerange().start(), zoneId)); } - this.hours = hours.toString(); + + this.hours = locHours.toString(); } @Override @@ -61,10 +84,6 @@ public boolean contains(long timestamp) { 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 bf623d24c170b..461292b66f7e8 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 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 String getHours() { 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 e2fb8585c2c74..6c08bad7aecda 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.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 void refreshChannels() { 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,9 +140,13 @@ public void refreshChannel(ChannelUID channelUID) { updateState(channelUID, state); return; } + + ZoneId zoneId = bridgeHandler.getTimeZone(); + AwattarBestPriceConfiguration config = getConfigAs(AwattarBestPriceConfiguration.class); - TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, bridgeHandler.getTimeZone()); - if (!(bridgeHandler.containsPriceFor(timerange.start()) && bridgeHandler.containsPriceFor(timerange.end()))) { + + TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, zoneId); + if (!(bridgeHandler.containsPriceFor(timerange))) { updateState(channelUID, state); return; } @@ -151,36 +155,11 @@ public void refreshChannel(ChannelUID channelUID) { 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/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 3eb991140189b..e11fcdfce81c3 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 @@ -239,8 +239,20 @@ public synchronized SortedSet getPrices() { public boolean containsPriceFor(long timestamp) { SortedSet localPrices = getPrices(); - return localPrices != null && localPrices.first().timerange().start() <= timestamp - && localPrices.last().timerange().end() > timestamp; + if (localPrices == null) { + return false; + } + return new TimeRange(localPrices.first().timerange().start(), localPrices.last().timerange().end()) + .contains(timestamp); + } + + public boolean containsPriceFor(TimeRange timeRange) { + SortedSet localPrices = getPrices(); + if (localPrices == null) { + return false; + } + return new TimeRange(localPrices.first().timerange().start(), localPrices.last().timerange().end()) + .contains(timeRange); } @Override diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/TimeRange.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/TimeRange.java index 7e3bf8815c164..2ff339f809700 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/TimeRange.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/TimeRange.java @@ -48,6 +48,7 @@ public boolean contains(TimeRange other) { * @param o the object to be compared * @return the result of {@link Long#compare(long, long)} for the {@link #start} timestamps */ + @Override public int compareTo(TimeRange o) { return Long.compare(start, o.start); } 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 0000000000000..c818985cbf00b --- /dev/null +++ b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/AwattarBestPriceTest.java @@ -0,0 +1,119 @@ +/** + * 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.BeforeEach; +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; + + 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; + } + + @BeforeEach + public void setUp() { + zoneId = ZoneId.of("GMT"); + } + + @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/handler/AwattarBridgeHandlerTest.java b/bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerTest.java index 819174b2aef80..37dde69ac6497 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 @@ -150,9 +150,17 @@ void testGetPriceForFail() { } @Test - void testContainsPrizeFor() { + void testContainsPriceForTimestamp() { + assertThat(bridgeHandler.containsPriceFor(new TimeRange(1618503200000L, 1718316000000L)), is(false)); + assertThat(bridgeHandler.containsPriceFor(new TimeRange(1618503200000L, 1718503200000L)), is(false)); + assertThat(bridgeHandler.containsPriceFor(new TimeRange(1718503200000L, 1718575200000L)), is(true)); + } + + @Test + void testContainsPriceForRange() { assertThat(bridgeHandler.containsPriceFor(1618503200000L), is(false)); assertThat(bridgeHandler.containsPriceFor(1718503200000L), is(true)); + assertThat(bridgeHandler.containsPriceFor(1718575200000L), is(false)); assertThat(bridgeHandler.containsPriceFor(1818503200000L), is(false)); }