]> git.basschouten.com Git - openhab-addons.git/commitdiff
[aWATTar] added inverted best price (#16877)
authortl-photography <thomas@tl-photography.at>
Fri, 12 Jul 2024 06:50:16 +0000 (08:50 +0200)
committerGitHub <noreply@github.com>
Fri, 12 Jul 2024 06:50:16 +0000 (08:50 +0200)
Signed-off-by: Thomas Leber <thomas@tl-photography.at>
bundles/org.openhab.binding.awattar/README.md
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestpriceConfiguration.java [deleted file]
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestPriceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestpriceHandler.java [deleted file]
bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java
bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/config/config.xml
bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/i18n/awattar.properties
bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/i18n/awattar_de.properties
bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.awattar/src/test/java/org/openhab/binding/awattar/internal/handler/AwattarBridgeHandlerTest.java

index ffa74f8528a74a50d79aa5c8545ef133b3ddf22c..104342a007f102d8f6a25d7a806951e6fff8a700 100644 (file)
@@ -26,12 +26,12 @@ Auto discovery is not supported.
 
 ### aWATTar Bridge
 
-| Parameter      | Description                                                                                                                |
-|-------------|-------------------------------------------------------------------------------------------------------------------------------|
-| vatPercent  | Percentage of the value added tax to apply to net prices. Optional, defaults to 19.                                           |
-| basePrice   | The net(!) base price you have to pay for every kWh. Optional, but you most probably want to set it based on you delivery contract.  |
-| timeZone    | The time zone the hour definitions of the things below refer to. Default is `CET`, as it corresponds to the aWATTar API. It is strongly recommended not to change this. However, if you do so, be aware that the prices delivered by the API will not cover a whole calendar day in this timezone. **Advanced** |
-| country     | The country prices should be received for. Use `DE` for Germany or `AT` for Austria. `DE` is the default. |
+| Parameter  | Description                                                                                                                                                                                                                                                                                                     |
+| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| vatPercent | Percentage of the value added tax to apply to net prices. Optional, defaults to 19.                                                                                                                                                                                                                             |
+| basePrice  | The net(!) base price you have to pay for every kWh. Optional, but you most probably want to set it based on you delivery contract.                                                                                                                                                                             |
+| timeZone   | The time zone the hour definitions of the things below refer to. Default is `CET`, as it corresponds to the aWATTar API. It is strongly recommended not to change this. However, if you do so, be aware that the prices delivered by the API will not cover a whole calendar day in this timezone. **Advanced** |
+| country    | The country prices should be received for. Use `DE` for Germany or `AT` for Austria. `DE` is the default.                                                                                                                                                                                                       |
 
 ### Prices Thing
 
@@ -39,12 +39,13 @@ The prices thing does not need any configuration.
 
 ### Bestprice Thing
 
-| Parameter      | Description                                                                                                                |
-|-------------|-------------------------------------------------------------------------------------------------------------------------------|
-| rangeStart  | First hour of the time range the binding should search for the best prices. Default: `0`                                     |
-| rangeDuration  | The duration of the time range the binding should search for best prices. Default: `24`   |
-| length      | number of best price hours to find within the range. This value has to be at least `1` and below `rangeDuration` Default: `1` |
-| consecutive | if `true`, the thing identifies the cheapest consecutive range of `length` hours within the lookup range. Otherwise, the thing contains the cheapest `length` hours within the lookup range. Default: `true` |
+| Parameter     | Description                                                                                                                                                                                                  |
+| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| rangeStart    | First hour of the time range the binding should search for the best prices. Default: `0`                                                                                                                     |
+| rangeDuration | The duration of the time range the binding should search for best prices. Default: `24`                                                                                                                      |
+| length        | number of best price hours to find within the range. This value has to be at least `1` and below `rangeDuration` Default: `1`                                                                                |
+| consecutive   | if `true`, the thing identifies the cheapest consecutive range of `length` hours within the lookup range. Otherwise, the thing contains the cheapest `length` hours within the lookup range. Default: `true` |
+| inverted      | if `true`, the worst prices will be searched instead of the best. Does currently not work in combination with 'consecutive'. Default: `false`                                                                |
 
 #### Limitations
 
@@ -59,31 +60,31 @@ Also, due to the time the aWATTar API delivers the data for the next day, it doe
 
 For every hour, the `prices` thing provides the following prices:
 
-| channel  | type   | description                  |
-|----------|--------|------------------------------|
-| market-net  | Number | This net market price per kWh. This is directly taken from the price the aWATTar API delivers.  |
-| market-gross  | Number | The market price including VAT, using the defined VAT percentage.  |
-| total-net | Number | Sum of net market price and configured base price |
-| total-gross | Number | Sum of market and base price with VAT applied. Most probably this is the final price you will have to pay for one kWh in a certain hour |
+| channel      | type   | description                                                                                                                             |
+| ------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------- |
+| market-net   | Number | This net market price per kWh. This is directly taken from the price the aWATTar API delivers.                                          |
+| market-gross | Number | The market price including VAT, using the defined VAT percentage.                                                                       |
+| total-net    | Number | Sum of net market price and configured base price                                                                                       |
+| total-gross  | Number | Sum of market and base price with VAT applied. Most probably this is the final price you will have to pay for one kWh in a certain hour |
 
 All prices are available in each of the following channel groups:
 
-| channel group | description                  |
-|----------|--------------------------------|
-| current | The prices for the current hour |
-| today00, today01, today02 ... today23  Hourly prices for today. `today00` provides the price from 0:00 to 1:00, `today01` from 1:00 to 02:00 and so on. As long as the API is working, this data should always be available |
-| tomorrow00, tomorrow01, ... tomorrow23 | Hourly prices for the next day. They should be available starting at  14:00. |
+| channel group                          | description                                                                                                                                                                          |
+| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| current                                | The prices for the current hour                                                                                                                                                      |
+| today00, today01, today02 ... today23  | Hourly prices for today. `today00` provides the price from 0:00 to 1:00, `today01` from 1:00 to 02:00 and so on. As long as the API is working, this data should always be available |
+| tomorrow00, tomorrow01, ... tomorrow23 | Hourly prices for the next day. They should be available starting at  14:00.                                                                                                         |
 
 ### Bestprice Thing
 
-| channel  | type        | description                                                                                                                                                                                              |
-|----------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| active | Switch      | `ON` if the current time is within the bestprice period, `OFF` otherwise. If `consecutive` was set to `false`, this channel may change between `ON` and `OFF` multiple times within the bestprice period. |
-| start  | DateTime    | The exact start time of the bestprice range. If `consecutive` was `false`, it is the start time of the first hour found.                                                                                 |
-| end  | DateTime    | The exact end time of the bestprice range. If `consecutive` was `false`, it is the end time of the last hour found.                                                                                      |
-| countdown  | Number:Time | The time in minutes until start of the bestprice range. If start time passed. the channel will be set to `UNDEFINED` until the values for the next day are available.                   |
-| remaining | Number:Time | The time in minutes until end of the bestprice range. If start time passed. the channel will be set to `UNDEFINED` until the values for the next day are available.                                      |
-| hours | String      | A comma separated list of hours this bestprice period contains.                                                                                                                                          |
+| channel   | type        | description                                                                                                                                                                                               |
+| --------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| active    | Switch      | `ON` if the current time is within the bestprice period, `OFF` otherwise. If `consecutive` was set to `false`, this channel may change between `ON` and `OFF` multiple times within the bestprice period. |
+| start     | DateTime    | The exact start time of the bestprice range. If `consecutive` was `false`, it is the start time of the first hour found.                                                                                  |
+| end       | DateTime    | The exact end time of the bestprice range. If `consecutive` was `false`, it is the end time of the last hour found.                                                                                       |
+| countdown | Number:Time | The time in minutes until start of the bestprice range. If start time passed. the channel will be set to `UNDEFINED` until the values for the next day are available.                                     |
+| remaining | Number:Time | The time in minutes until end of the bestprice range. If start time passed. the channel will be set to `UNDEFINED` until the values for the next day are available.                                       |
+| hours     | String      | A comma separated list of hours this bestprice period contains.                                                                                                                                           |
 
 ## Full Example
 
@@ -93,7 +94,7 @@ awattar.things:
 
 ```java
 Bridge awattar:bridge:bridge1 "aWATTar Bridge" [ country="DE", vatPercent="19", basePrice="17.22"] {
- Thing prices price1 "aWATTar Price" [] 
+ Thing prices price1 "aWATTar Price" []
 // The car should be loaded for 4 hours during the night
  Thing bestprice carloader "Car Loader" [ rangeStart="22", rangeDuration="8", length="4", consecutive="true" ]
 // In the cheapest hour of the night the garden should be watered
diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceConfiguration.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceConfiguration.java
new file mode 100644 (file)
index 0000000..7af03bb
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * 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 org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Stores the bestprice config
+ *
+ * @author Wolfgang Klimt - initial contribution
+ */
+@NonNullByDefault
+public class AwattarBestPriceConfiguration {
+    public int rangeStart = 0;
+    public int rangeDuration = 24;
+    public int length = 1;
+    public boolean consecutive = true;
+    public boolean inverted = false;
+
+    @Override
+    public String toString() {
+        return String.format("{ s: %d, d: %d, l: %d, c: %b )", rangeStart, rangeDuration, length, consecutive);
+    }
+}
diff --git a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestpriceConfiguration.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestpriceConfiguration.java
deleted file mode 100644 (file)
index 8ab8d82..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * 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 org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * Stores the bestprice config
- *
- * @author Wolfgang Klimt - initial contribution
- */
-@NonNullByDefault
-public class AwattarBestpriceConfiguration {
-    public int rangeStart = 0;
-    public int rangeDuration = 24;
-    public int length = 1;
-    public boolean consecutive = true;
-
-    @Override
-    public String toString() {
-        return String.format("{ s: %d, d: %d, l: %d, c: %b )", rangeStart, rangeDuration, length, consecutive);
-    }
-}
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
new file mode 100644 (file)
index 0000000..ca0e911
--- /dev/null
@@ -0,0 +1,251 @@
+/**
+ * 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.handler;
+
+import static org.openhab.binding.awattar.internal.AwattarBindingConstants.BINDING_ID;
+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 static org.openhab.binding.awattar.internal.AwattarUtil.getCalendarForHour;
+import static org.openhab.binding.awattar.internal.AwattarUtil.getDateTimeType;
+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;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.awattar.internal.AwattarBestPriceConfiguration;
+import org.openhab.binding.awattar.internal.AwattarBestPriceResult;
+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.core.library.types.OnOffType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.type.ChannelKind;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AwattarBestPriceHandler} is responsible for computing the best prices for a given configuration.
+ *
+ * @author Wolfgang Klimt - Initial contribution
+ */
+@NonNullByDefault
+public class AwattarBestPriceHandler extends BaseThingHandler {
+    private static final int THING_REFRESH_INTERVAL = 60;
+
+    private final Logger logger = LoggerFactory.getLogger(AwattarBestPriceHandler.class);
+
+    private @Nullable ScheduledFuture<?> thingRefresher;
+
+    private final TimeZoneProvider timeZoneProvider;
+
+    public AwattarBestPriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
+        super(thing);
+        this.timeZoneProvider = timeZoneProvider;
+    }
+
+    @Override
+    public void initialize() {
+        AwattarBestPriceConfiguration config = getConfigAs(AwattarBestPriceConfiguration.class);
+
+        if (config.length >= config.rangeDuration) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.length.value");
+            return;
+        }
+
+        synchronized (this) {
+            ScheduledFuture<?> localRefresher = thingRefresher;
+            if (localRefresher == null || localRefresher.isCancelled()) {
+                /*
+                 * The scheduler is required to run exactly at minute borders, hence we can't use scheduleWithFixedDelay
+                 * here
+                 */
+                thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
+                        getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000,
+                        TimeUnit.MILLISECONDS);
+            }
+        }
+        updateStatus(ThingStatus.UNKNOWN);
+    }
+
+    @Override
+    public void dispose() {
+        ScheduledFuture<?> localRefresher = thingRefresher;
+        if (localRefresher != null) {
+            localRefresher.cancel(true);
+            thingRefresher = null;
+        }
+    }
+
+    public void refreshChannels() {
+        updateStatus(ThingStatus.ONLINE);
+        for (Channel channel : getThing().getChannels()) {
+            ChannelUID channelUID = channel.getUID();
+            if (ChannelKind.STATE.equals(channel.getKind()) && isLinked(channelUID)) {
+                refreshChannel(channel.getUID());
+            }
+        }
+    }
+
+    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.");
+            // no prices available, can't continue
+            updateState(channelUID, state);
+            return;
+        }
+        AwattarBestPriceConfiguration config = getConfigAs(AwattarBestPriceConfiguration.class);
+        TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, bridgeHandler.getTimeZone());
+        if (!(bridgeHandler.containsPriceFor(timerange.start()) && bridgeHandler.containsPriceFor(timerange.end()))) {
+            updateState(channelUID, state);
+            return;
+        }
+
+        AwattarBestPriceResult result;
+        List<AwattarPrice> 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;
+        } 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;
+        }
+        String channelId = channelUID.getIdWithoutGroup();
+        long diff;
+        switch (channelId) {
+            case CHANNEL_ACTIVE:
+                state = OnOffType.from(result.isActive());
+                break;
+            case CHANNEL_START:
+                state = getDateTimeType(result.getStart(), timeZoneProvider);
+                break;
+            case CHANNEL_END:
+                state = getDateTimeType(result.getEnd(), timeZoneProvider);
+                break;
+            case CHANNEL_COUNTDOWN:
+                diff = result.getStart() - Instant.now().toEpochMilli();
+                if (diff >= 0) {
+                    state = getDuration(diff);
+                }
+                break;
+            case CHANNEL_REMAINING:
+                diff = result.getEnd() - Instant.now().toEpochMilli();
+                if (result.isActive()) {
+                    state = getDuration(diff);
+                }
+                break;
+            case CHANNEL_HOURS:
+                state = new StringType(result.getHours());
+                break;
+            default:
+                logger.warn("Unknown channel id {} for Thing type {}", channelUID, getThing().getThingTypeUID());
+        }
+        updateState(channelUID, state);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        if (command instanceof RefreshType) {
+            refreshChannel(channelUID);
+        } else {
+            logger.debug("Binding {} only supports refresh command", BINDING_ID);
+        }
+    }
+
+    private List<AwattarPrice> getPriceRange(AwattarBridgeHandler bridgeHandler, TimeRange range) {
+        List<AwattarPrice> result = new ArrayList<>();
+        SortedSet<AwattarPrice> prices = bridgeHandler.getPrices();
+        if (prices == null) {
+            logger.debug("No prices available, can't compute ranges");
+            return result;
+        }
+        result.addAll(prices.stream().filter(x -> range.contains(x.timerange())).toList());
+        return result;
+    }
+
+    protected TimeRange getRange(int start, int duration, ZoneId zoneId) {
+        ZonedDateTime startCal = getCalendarForHour(start, zoneId);
+        ZonedDateTime endCal = startCal.plusHours(duration);
+        ZonedDateTime now = ZonedDateTime.now(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);
+        }
+        if (endCal.toInstant().toEpochMilli() < Instant.now().toEpochMilli()) {
+            // span is in the past, add one day
+            startCal = startCal.plusDays(1);
+            endCal = endCal.plusDays(1);
+        }
+        return new TimeRange(startCal.toInstant().toEpochMilli(), endCal.toInstant().toEpochMilli());
+    }
+}
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
deleted file mode 100644 (file)
index 4c92b52..0000000
+++ /dev/null
@@ -1,245 +0,0 @@
-/**
- * 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.handler;
-
-import static org.openhab.binding.awattar.internal.AwattarBindingConstants.BINDING_ID;
-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 static org.openhab.binding.awattar.internal.AwattarUtil.getCalendarForHour;
-import static org.openhab.binding.awattar.internal.AwattarUtil.getDateTimeType;
-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;
-import java.util.Comparator;
-import java.util.List;
-import java.util.SortedSet;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.awattar.internal.AwattarBestPriceResult;
-import org.openhab.binding.awattar.internal.AwattarBestpriceConfiguration;
-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.core.library.types.OnOffType;
-import org.openhab.core.library.types.StringType;
-import org.openhab.core.thing.Bridge;
-import org.openhab.core.thing.Channel;
-import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.ThingStatus;
-import org.openhab.core.thing.ThingStatusDetail;
-import org.openhab.core.thing.binding.BaseThingHandler;
-import org.openhab.core.thing.type.ChannelKind;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
-import org.openhab.core.types.State;
-import org.openhab.core.types.UnDefType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * The {@link AwattarBestpriceHandler} is responsible for computing the best prices for a given configuration.
- *
- * @author Wolfgang Klimt - Initial contribution
- */
-@NonNullByDefault
-public class AwattarBestpriceHandler extends BaseThingHandler {
-    private static final int THING_REFRESH_INTERVAL = 60;
-
-    private final Logger logger = LoggerFactory.getLogger(AwattarBestpriceHandler.class);
-
-    private @Nullable ScheduledFuture<?> thingRefresher;
-
-    private final TimeZoneProvider timeZoneProvider;
-
-    public AwattarBestpriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
-        super(thing);
-        this.timeZoneProvider = timeZoneProvider;
-    }
-
-    @Override
-    public void initialize() {
-        AwattarBestpriceConfiguration config = getConfigAs(AwattarBestpriceConfiguration.class);
-
-        if (config.length >= config.rangeDuration) {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.length.value");
-            return;
-        }
-
-        synchronized (this) {
-            ScheduledFuture<?> localRefresher = thingRefresher;
-            if (localRefresher == null || localRefresher.isCancelled()) {
-                /*
-                 * The scheduler is required to run exactly at minute borders, hence we can't use scheduleWithFixedDelay
-                 * here
-                 */
-                thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
-                        getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000,
-                        TimeUnit.MILLISECONDS);
-            }
-        }
-        updateStatus(ThingStatus.UNKNOWN);
-    }
-
-    @Override
-    public void dispose() {
-        ScheduledFuture<?> localRefresher = thingRefresher;
-        if (localRefresher != null) {
-            localRefresher.cancel(true);
-            thingRefresher = null;
-        }
-    }
-
-    public void refreshChannels() {
-        updateStatus(ThingStatus.ONLINE);
-        for (Channel channel : getThing().getChannels()) {
-            ChannelUID channelUID = channel.getUID();
-            if (ChannelKind.STATE.equals(channel.getKind()) && isLinked(channelUID)) {
-                refreshChannel(channel.getUID());
-            }
-        }
-    }
-
-    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.");
-            // no prices available, can't continue
-            updateState(channelUID, state);
-            return;
-        }
-        AwattarBestpriceConfiguration config = getConfigAs(AwattarBestpriceConfiguration.class);
-        TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, bridgeHandler.getTimeZone());
-        if (!(bridgeHandler.containsPriceFor(timerange.start()) && bridgeHandler.containsPriceFor(timerange.end()))) {
-            updateState(channelUID, state);
-            return;
-        }
-
-        AwattarBestPriceResult result;
-        List<AwattarPrice> 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;
-        } else {
-            range.sort(Comparator.naturalOrder());
-            AwattarNonConsecutiveBestPriceResult res = new AwattarNonConsecutiveBestPriceResult(
-                    bridgeHandler.getTimeZone());
-            int ct = 0;
-            for (AwattarPrice price : range) {
-                res.addMember(price);
-                if (++ct >= config.length) {
-                    break;
-                }
-            }
-            result = res;
-        }
-        String channelId = channelUID.getIdWithoutGroup();
-        long diff;
-        switch (channelId) {
-            case CHANNEL_ACTIVE:
-                state = OnOffType.from(result.isActive());
-                break;
-            case CHANNEL_START:
-                state = getDateTimeType(result.getStart(), timeZoneProvider);
-                break;
-            case CHANNEL_END:
-                state = getDateTimeType(result.getEnd(), timeZoneProvider);
-                break;
-            case CHANNEL_COUNTDOWN:
-                diff = result.getStart() - Instant.now().toEpochMilli();
-                if (diff >= 0) {
-                    state = getDuration(diff);
-                }
-                break;
-            case CHANNEL_REMAINING:
-                diff = result.getEnd() - Instant.now().toEpochMilli();
-                if (result.isActive()) {
-                    state = getDuration(diff);
-                }
-                break;
-            case CHANNEL_HOURS:
-                state = new StringType(result.getHours());
-                break;
-            default:
-                logger.warn("Unknown channel id {} for Thing type {}", channelUID, getThing().getThingTypeUID());
-        }
-        updateState(channelUID, state);
-    }
-
-    @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
-        if (command instanceof RefreshType) {
-            refreshChannel(channelUID);
-        } else {
-            logger.debug("Binding {} only supports refresh command", BINDING_ID);
-        }
-    }
-
-    private List<AwattarPrice> getPriceRange(AwattarBridgeHandler bridgeHandler, TimeRange range) {
-        List<AwattarPrice> result = new ArrayList<>();
-        SortedSet<AwattarPrice> prices = bridgeHandler.getPrices();
-        if (prices == null) {
-            logger.debug("No prices available, can't compute ranges");
-            return result;
-        }
-        result.addAll(prices.stream().filter(x -> range.contains(x.timerange())).toList());
-        return result;
-    }
-
-    protected TimeRange getRange(int start, int duration, ZoneId zoneId) {
-        ZonedDateTime startCal = getCalendarForHour(start, zoneId);
-        ZonedDateTime endCal = startCal.plusHours(duration);
-        ZonedDateTime now = ZonedDateTime.now(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);
-        }
-        if (endCal.toInstant().toEpochMilli() < Instant.now().toEpochMilli()) {
-            // span is in the past, add one day
-            startCal = startCal.plusDays(1);
-            endCal = endCal.plusDays(1);
-        }
-        return new TimeRange(startCal.toInstant().toEpochMilli(), endCal.toInstant().toEpochMilli());
-    }
-}
index c232edfcc72ff338ffde2a1d430e147821cc7fac..50ca43afc91d21ec700c3817e2ee4b7180eaf2c5 100644 (file)
@@ -36,7 +36,8 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * The {@link AwattarHandlerFactory} is responsible for creating things and thing
+ * The {@link AwattarHandlerFactory} is responsible for creating things and
+ * thing
  * handlers.
  *
  * @author Wolfgang Klimt - Initial contribution
@@ -72,7 +73,7 @@ public class AwattarHandlerFactory extends BaseThingHandlerFactory {
         } else if (THING_TYPE_PRICE.equals(thingTypeUID)) {
             return new AwattarPriceHandler(thing, timeZoneProvider);
         } else if (THING_TYPE_BESTPRICE.equals(thingTypeUID)) {
-            return new AwattarBestpriceHandler(thing, timeZoneProvider);
+            return new AwattarBestPriceHandler(thing, timeZoneProvider);
         }
 
         logger.warn("Unknown thing type {}, not creating handler!", thingTypeUID);
index 619864cc0700834e93fcaae2e705d59affe06f6a..10445470f928541bc4c2960793b23457e9898640 100644 (file)
                        <description>Do the hours need to be consecutive?</description>
                        <default>true</default>
                </parameter>
+               <parameter name="inverted" type="boolean">
+                       <label>Inverted</label>
+                       <description>Should the highest prices be returned?</description>
+                       <default>false</default>
+               </parameter>
        </config-description>
 
 </config-description:config-descriptions>
index 3fc96bad710c52b8a26a11dfc5c2e5e2f43a4c9b..6df6886f3af94f65bf497a79586541ec52b6d16e 100644 (file)
@@ -28,6 +28,8 @@ thing-type.config.awattar.bestprice.length.label = Length
 thing-type.config.awattar.bestprice.length.description = The number of hours the bestprice period should last
 thing-type.config.awattar.bestprice.consecutive.label = Consecutive
 thing-type.config.awattar.bestprice.consecutive.description = Do the hours need to be consecutive?
+thing-type.config.awattar.bestprice.inverted.label = Inverted
+thing-type.config.awattar.bestprice.inverted.description = Invert the search for the highest price
 
 # channel types
 channel-type.awattar.price.label = ct/kWh
index 2888dab20781ea23d3a211c4a74540faf1b7d734..24e6563806ae43ed0c772241f1ce2646360e2bf0 100644 (file)
@@ -28,6 +28,8 @@ thing-type.config.awattar.bestprice.length.label = Länge
 thing-type.config.awattar.bestprice.length.description = Die Anzahl der zu findenden günstigen Stunden
 thing-type.config.awattar.bestprice.consecutive.label = Durchgehend
 thing-type.config.awattar.bestprice.consecutive.description = Wird ein einzelner durchgehender Zeitraum gesucht?
+thing-type.config.awattar.bestprice.inverted.label = Invertiert
+thing-type.config.awattar.bestprice.inverted.description = Wird nach den teuersten Stunden gesucht?
 
 # channel types
 channel-type.awattar.price.label = ct/kWh
index 4b659c087b22eb8f4dd33c1a7682bdc04a0c53f3..837435424b7f6c5cc1c39b1387975188dfa3a31a 100644 (file)
                <state readOnly="false"/>
        </channel-type>
 
+       <channel-type id="input-inverted">
+               <item-type>Switch</item-type>
+               <label>Inverted</label>
+               <description>Return highest prices?</description>
+               <state readOnly="false"/>
+       </channel-type>
+
 
        <channel-type id="switch-type">
                <item-type>Switch</item-type>
index a0e8d079c0897bc0c22e8a0768588d3fcc17d38a..b4d5ace5b8004f770101f27e93c44f0740e04e0c 100644 (file)
@@ -177,7 +177,7 @@ public class AwattarBridgeHandlerTest extends JavaTest {
         Map<String, Object> config = Map.of("length", length, "consecutive", consecutive);
         when(bestpriceMock.getConfiguration()).thenReturn(new Configuration(config));
 
-        AwattarBestpriceHandler handler = new AwattarBestpriceHandler(bestpriceMock, timeZoneProviderMock) {
+        AwattarBestPriceHandler handler = new AwattarBestPriceHandler(bestpriceMock, timeZoneProviderMock) {
             @Override
             protected TimeRange getRange(int start, int duration, ZoneId zoneId) {
                 return new TimeRange(1718402400000L, 1718488800000L);