2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.awattar.internal.handler;
15 import static org.openhab.binding.awattar.internal.AwattarBindingConstants.BINDING_ID;
16 import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_ACTIVE;
17 import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_COUNTDOWN;
18 import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_END;
19 import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_HOURS;
20 import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_REMAINING;
21 import static org.openhab.binding.awattar.internal.AwattarBindingConstants.CHANNEL_START;
22 import static org.openhab.binding.awattar.internal.AwattarUtil.getCalendarForHour;
23 import static org.openhab.binding.awattar.internal.AwattarUtil.getDateTimeType;
24 import static org.openhab.binding.awattar.internal.AwattarUtil.getDuration;
25 import static org.openhab.binding.awattar.internal.AwattarUtil.getMillisToNextMinute;
27 import java.time.Instant;
28 import java.time.ZoneId;
29 import java.time.ZonedDateTime;
30 import java.util.ArrayList;
31 import java.util.Collections;
32 import java.util.Comparator;
33 import java.util.List;
34 import java.util.SortedSet;
35 import java.util.concurrent.ScheduledFuture;
36 import java.util.concurrent.TimeUnit;
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.awattar.internal.AwattarBestPriceConfiguration;
41 import org.openhab.binding.awattar.internal.AwattarBestPriceResult;
42 import org.openhab.binding.awattar.internal.AwattarConsecutiveBestPriceResult;
43 import org.openhab.binding.awattar.internal.AwattarNonConsecutiveBestPriceResult;
44 import org.openhab.binding.awattar.internal.AwattarPrice;
45 import org.openhab.core.i18n.TimeZoneProvider;
46 import org.openhab.core.library.types.OnOffType;
47 import org.openhab.core.library.types.StringType;
48 import org.openhab.core.thing.Bridge;
49 import org.openhab.core.thing.Channel;
50 import org.openhab.core.thing.ChannelUID;
51 import org.openhab.core.thing.Thing;
52 import org.openhab.core.thing.ThingStatus;
53 import org.openhab.core.thing.ThingStatusDetail;
54 import org.openhab.core.thing.binding.BaseThingHandler;
55 import org.openhab.core.thing.type.ChannelKind;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.openhab.core.types.State;
59 import org.openhab.core.types.UnDefType;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
64 * The {@link AwattarBestPriceHandler} is responsible for computing the best prices for a given configuration.
66 * @author Wolfgang Klimt - Initial contribution
69 public class AwattarBestPriceHandler extends BaseThingHandler {
70 private static final int THING_REFRESH_INTERVAL = 60;
72 private final Logger logger = LoggerFactory.getLogger(AwattarBestPriceHandler.class);
74 private @Nullable ScheduledFuture<?> thingRefresher;
76 private final TimeZoneProvider timeZoneProvider;
78 public AwattarBestPriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
80 this.timeZoneProvider = timeZoneProvider;
84 public void initialize() {
85 AwattarBestPriceConfiguration config = getConfigAs(AwattarBestPriceConfiguration.class);
87 if (config.length >= config.rangeDuration) {
88 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.length.value");
93 ScheduledFuture<?> localRefresher = thingRefresher;
94 if (localRefresher == null || localRefresher.isCancelled()) {
96 * The scheduler is required to run exactly at minute borders, hence we can't use scheduleWithFixedDelay
99 thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
100 getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000,
101 TimeUnit.MILLISECONDS);
104 updateStatus(ThingStatus.UNKNOWN);
108 public void dispose() {
109 ScheduledFuture<?> localRefresher = thingRefresher;
110 if (localRefresher != null) {
111 localRefresher.cancel(true);
112 thingRefresher = null;
116 public void refreshChannels() {
117 updateStatus(ThingStatus.ONLINE);
118 for (Channel channel : getThing().getChannels()) {
119 ChannelUID channelUID = channel.getUID();
120 if (ChannelKind.STATE.equals(channel.getKind()) && isLinked(channelUID)) {
121 refreshChannel(channel.getUID());
126 public void refreshChannel(ChannelUID channelUID) {
127 State state = UnDefType.UNDEF;
128 Bridge bridge = getBridge();
129 if (bridge == null) {
130 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.bridge.missing");
131 updateState(channelUID, state);
134 AwattarBridgeHandler bridgeHandler = (AwattarBridgeHandler) bridge.getHandler();
135 if (bridgeHandler == null || bridgeHandler.getPrices() == null) {
136 logger.debug("No prices available, so can't refresh channel.");
137 // no prices available, can't continue
138 updateState(channelUID, state);
141 AwattarBestPriceConfiguration config = getConfigAs(AwattarBestPriceConfiguration.class);
142 TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, bridgeHandler.getTimeZone());
143 if (!(bridgeHandler.containsPriceFor(timerange.start()) && bridgeHandler.containsPriceFor(timerange.end()))) {
144 updateState(channelUID, state);
148 AwattarBestPriceResult result;
149 List<AwattarPrice> range = getPriceRange(bridgeHandler, timerange);
151 if (config.consecutive) {
152 range.sort(Comparator.comparing(AwattarPrice::timerange));
153 AwattarConsecutiveBestPriceResult res = new AwattarConsecutiveBestPriceResult(
154 range.subList(0, config.length), bridgeHandler.getTimeZone());
156 for (int i = 1; i <= range.size() - config.length; i++) {
157 AwattarConsecutiveBestPriceResult res2 = new AwattarConsecutiveBestPriceResult(
158 range.subList(i, i + config.length), bridgeHandler.getTimeZone());
159 if (res2.getPriceSum() < res.getPriceSum()) {
165 range.sort(Comparator.naturalOrder());
167 // sort in descending order when inverted
168 if (config.inverted) {
169 Collections.reverse(range);
172 AwattarNonConsecutiveBestPriceResult res = new AwattarNonConsecutiveBestPriceResult(
173 bridgeHandler.getTimeZone());
175 // take up to config.length prices
176 for (int i = 0; i < Math.min(config.length, range.size()); i++) {
177 res.addMember(range.get(i));
182 String channelId = channelUID.getIdWithoutGroup();
186 state = OnOffType.from(result.isActive());
189 state = getDateTimeType(result.getStart(), timeZoneProvider);
192 state = getDateTimeType(result.getEnd(), timeZoneProvider);
194 case CHANNEL_COUNTDOWN:
195 diff = result.getStart() - Instant.now().toEpochMilli();
197 state = getDuration(diff);
200 case CHANNEL_REMAINING:
201 diff = result.getEnd() - Instant.now().toEpochMilli();
202 if (result.isActive()) {
203 state = getDuration(diff);
207 state = new StringType(result.getHours());
210 logger.warn("Unknown channel id {} for Thing type {}", channelUID, getThing().getThingTypeUID());
212 updateState(channelUID, state);
216 public void handleCommand(ChannelUID channelUID, Command command) {
217 if (command instanceof RefreshType) {
218 refreshChannel(channelUID);
220 logger.debug("Binding {} only supports refresh command", BINDING_ID);
224 private List<AwattarPrice> getPriceRange(AwattarBridgeHandler bridgeHandler, TimeRange range) {
225 List<AwattarPrice> result = new ArrayList<>();
226 SortedSet<AwattarPrice> prices = bridgeHandler.getPrices();
227 if (prices == null) {
228 logger.debug("No prices available, can't compute ranges");
231 result.addAll(prices.stream().filter(x -> range.contains(x.timerange())).toList());
235 protected TimeRange getRange(int start, int duration, ZoneId zoneId) {
236 ZonedDateTime startCal = getCalendarForHour(start, zoneId);
237 ZonedDateTime endCal = startCal.plusHours(duration);
238 ZonedDateTime now = ZonedDateTime.now(zoneId);
239 if (now.getHour() < start) {
240 // we are before the range, so we might be still within the last range
241 startCal = startCal.minusDays(1);
242 endCal = endCal.minusDays(1);
244 if (endCal.toInstant().toEpochMilli() < Instant.now().toEpochMilli()) {
245 // span is in the past, add one day
246 startCal = startCal.plusDays(1);
247 endCal = endCal.plusDays(1);
249 return new TimeRange(startCal.toInstant().toEpochMilli(), endCal.toInstant().toEpochMilli());