From: tl-photography Date: Fri, 12 Jul 2024 06:50:16 +0000 (+0200) Subject: [aWATTar] added inverted best price (#16877) X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=f7bcbe157513c957116eacad1a9b13ccd059c712;p=openhab-addons.git [aWATTar] added inverted best price (#16877) Signed-off-by: Thomas Leber --- diff --git a/bundles/org.openhab.binding.awattar/README.md b/bundles/org.openhab.binding.awattar/README.md index ffa74f8528..104342a007 100644 --- a/bundles/org.openhab.binding.awattar/README.md +++ b/bundles/org.openhab.binding.awattar/README.md @@ -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 index 0000000000..7af03bbb53 --- /dev/null +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestPriceConfiguration.java @@ -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 index 8ab8d82a35..0000000000 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/AwattarBestpriceConfiguration.java +++ /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 index 0000000000..ca0e911942 --- /dev/null +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestPriceHandler.java @@ -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 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 getPriceRange(AwattarBridgeHandler bridgeHandler, TimeRange range) { + List result = new ArrayList<>(); + SortedSet 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 index 4c92b52341..0000000000 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarBestpriceHandler.java +++ /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 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 getPriceRange(AwattarBridgeHandler bridgeHandler, TimeRange range) { - List result = new ArrayList<>(); - SortedSet 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/AwattarHandlerFactory.java b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java index c232edfcc7..50ca43afc9 100644 --- a/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java +++ b/bundles/org.openhab.binding.awattar/src/main/java/org/openhab/binding/awattar/internal/handler/AwattarHandlerFactory.java @@ -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); diff --git a/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/config/config.xml index 619864cc07..10445470f9 100644 --- a/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/config/config.xml @@ -47,6 +47,11 @@ Do the hours need to be consecutive? true + + + Should the highest prices be returned? + false + diff --git a/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/i18n/awattar.properties b/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/i18n/awattar.properties index 3fc96bad71..6df6886f3a 100644 --- a/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/i18n/awattar.properties +++ b/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/i18n/awattar.properties @@ -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 diff --git a/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/i18n/awattar_de.properties b/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/i18n/awattar_de.properties index 2888dab207..24e6563806 100644 --- a/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/i18n/awattar_de.properties +++ b/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/i18n/awattar_de.properties @@ -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 diff --git a/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/thing/thing-types.xml index 4b659c087b..837435424b 100644 --- a/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.awattar/src/main/resources/OH-INF/thing/thing-types.xml @@ -278,6 +278,13 @@ + + Switch + + Return highest prices? + + + Switch 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 a0e8d079c0..b4d5ace5b8 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 @@ -177,7 +177,7 @@ public class AwattarBridgeHandlerTest extends JavaTest { Map 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);