### 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
### 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
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
```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
--- /dev/null
+/**
+ * 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);
+ }
+}
+++ /dev/null
-/**
- * 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);
- }
-}
--- /dev/null
+/**
+ * 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());
+ }
+}
+++ /dev/null
-/**
- * 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());
- }
-}
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
} 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);
<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>
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
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
<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>
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);