]> git.basschouten.com Git - openhab-addons.git/blob
7645308313ece6ecc571ed6a22f463ed24673125
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.awattar.internal.handler;
14
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;
26
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;
37
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.QuantityType;
48 import org.openhab.core.library.types.StringType;
49 import org.openhab.core.library.unit.Units;
50 import org.openhab.core.thing.Bridge;
51 import org.openhab.core.thing.Channel;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatus;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.binding.BaseThingHandler;
57 import org.openhab.core.thing.type.ChannelKind;
58 import org.openhab.core.types.Command;
59 import org.openhab.core.types.RefreshType;
60 import org.openhab.core.types.State;
61 import org.openhab.core.types.UnDefType;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 /**
66  * The {@link AwattarBestPriceHandler} is responsible for computing the best prices for a given configuration.
67  *
68  * @author Wolfgang Klimt - Initial contribution
69  */
70 @NonNullByDefault
71 public class AwattarBestPriceHandler extends BaseThingHandler {
72     private static final int THING_REFRESH_INTERVAL = 60;
73
74     private final Logger logger = LoggerFactory.getLogger(AwattarBestPriceHandler.class);
75
76     private @Nullable ScheduledFuture<?> thingRefresher;
77
78     private final TimeZoneProvider timeZoneProvider;
79
80     public AwattarBestPriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
81         super(thing);
82         this.timeZoneProvider = timeZoneProvider;
83     }
84
85     @Override
86     public void initialize() {
87         AwattarBestPriceConfiguration config = getConfigAs(AwattarBestPriceConfiguration.class);
88
89         if (config.length >= config.rangeDuration) {
90             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.length.value");
91             return;
92         }
93
94         synchronized (this) {
95             ScheduledFuture<?> localRefresher = thingRefresher;
96             if (localRefresher == null || localRefresher.isCancelled()) {
97                 /*
98                  * The scheduler is required to run exactly at minute borders, hence we can't use scheduleWithFixedDelay
99                  * here
100                  */
101                 thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
102                         getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000,
103                         TimeUnit.MILLISECONDS);
104             }
105         }
106         updateStatus(ThingStatus.UNKNOWN);
107     }
108
109     @Override
110     public void dispose() {
111         ScheduledFuture<?> localRefresher = thingRefresher;
112         if (localRefresher != null) {
113             localRefresher.cancel(true);
114             thingRefresher = null;
115         }
116     }
117
118     public void refreshChannels() {
119         updateStatus(ThingStatus.ONLINE);
120         for (Channel channel : getThing().getChannels()) {
121             ChannelUID channelUID = channel.getUID();
122             if (ChannelKind.STATE.equals(channel.getKind()) && isLinked(channelUID)) {
123                 refreshChannel(channel.getUID());
124             }
125         }
126     }
127
128     public void refreshChannel(ChannelUID channelUID) {
129         State state = UnDefType.UNDEF;
130         Bridge bridge = getBridge();
131         if (bridge == null) {
132             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.bridge.missing");
133             updateState(channelUID, state);
134             return;
135         }
136         AwattarBridgeHandler bridgeHandler = (AwattarBridgeHandler) bridge.getHandler();
137         if (bridgeHandler == null || bridgeHandler.getPrices() == null) {
138             logger.debug("No prices available, so can't refresh channel.");
139             // no prices available, can't continue
140             updateState(channelUID, state);
141             return;
142         }
143         AwattarBestPriceConfiguration config = getConfigAs(AwattarBestPriceConfiguration.class);
144         TimeRange timerange = getRange(config.rangeStart, config.rangeDuration, bridgeHandler.getTimeZone());
145         if (!(bridgeHandler.containsPriceFor(timerange.start()) && bridgeHandler.containsPriceFor(timerange.end()))) {
146             updateState(channelUID, state);
147             return;
148         }
149
150         AwattarBestPriceResult result;
151         List<AwattarPrice> range = getPriceRange(bridgeHandler, timerange);
152
153         if (config.consecutive) {
154             range.sort(Comparator.comparing(AwattarPrice::timerange));
155             AwattarConsecutiveBestPriceResult res = new AwattarConsecutiveBestPriceResult(
156                     range.subList(0, config.length), bridgeHandler.getTimeZone());
157
158             for (int i = 1; i <= range.size() - config.length; i++) {
159                 AwattarConsecutiveBestPriceResult res2 = new AwattarConsecutiveBestPriceResult(
160                         range.subList(i, i + config.length), bridgeHandler.getTimeZone());
161                 if (res2.getPriceSum() < res.getPriceSum()) {
162                     res = res2;
163                 }
164             }
165             result = res;
166         } else {
167             range.sort(Comparator.naturalOrder());
168
169             // sort in descending order when inverted
170             if (config.inverted) {
171                 Collections.reverse(range);
172             }
173
174             AwattarNonConsecutiveBestPriceResult res = new AwattarNonConsecutiveBestPriceResult(
175                     bridgeHandler.getTimeZone());
176
177             // take up to config.length prices
178             for (int i = 0; i < Math.min(config.length, range.size()); i++) {
179                 res.addMember(range.get(i));
180             }
181
182             result = res;
183         }
184         String channelId = channelUID.getIdWithoutGroup();
185         long diff;
186         switch (channelId) {
187             case CHANNEL_ACTIVE:
188                 state = OnOffType.from(result.isActive());
189                 break;
190             case CHANNEL_START:
191                 state = getDateTimeType(result.getStart(), timeZoneProvider);
192                 break;
193             case CHANNEL_END:
194                 state = getDateTimeType(result.getEnd(), timeZoneProvider);
195                 break;
196             case CHANNEL_COUNTDOWN:
197                 diff = result.getStart() - Instant.now().toEpochMilli();
198                 if (diff >= 0) {
199                     state = getDuration(diff);
200                 } else {
201                     state = QuantityType.valueOf(0, Units.MINUTE);
202                 }
203                 break;
204             case CHANNEL_REMAINING:
205                 if (result.isActive()) {
206                     diff = result.getEnd() - Instant.now().toEpochMilli();
207                     state = getDuration(diff);
208                 } else {
209                     state = QuantityType.valueOf(0, Units.MINUTE);
210                 }
211                 break;
212             case CHANNEL_HOURS:
213                 state = new StringType(result.getHours());
214                 break;
215             default:
216                 logger.warn("Unknown channel id {} for Thing type {}", channelUID, getThing().getThingTypeUID());
217         }
218         updateState(channelUID, state);
219     }
220
221     @Override
222     public void handleCommand(ChannelUID channelUID, Command command) {
223         if (command instanceof RefreshType) {
224             refreshChannel(channelUID);
225         } else {
226             logger.debug("Binding {} only supports refresh command", BINDING_ID);
227         }
228     }
229
230     private List<AwattarPrice> getPriceRange(AwattarBridgeHandler bridgeHandler, TimeRange range) {
231         List<AwattarPrice> result = new ArrayList<>();
232         SortedSet<AwattarPrice> prices = bridgeHandler.getPrices();
233         if (prices == null) {
234             logger.debug("No prices available, can't compute ranges");
235             return result;
236         }
237         result.addAll(prices.stream().filter(x -> range.contains(x.timerange())).toList());
238         return result;
239     }
240
241     protected TimeRange getRange(int start, int duration, ZoneId zoneId) {
242         ZonedDateTime startCal = getCalendarForHour(start, zoneId);
243         ZonedDateTime endCal = startCal.plusHours(duration);
244         ZonedDateTime now = ZonedDateTime.now(zoneId);
245         if (now.getHour() < start) {
246             // we are before the range, so we might be still within the last range
247             startCal = startCal.minusDays(1);
248             endCal = endCal.minusDays(1);
249         }
250         if (endCal.toInstant().toEpochMilli() < Instant.now().toEpochMilli()) {
251             // span is in the past, add one day
252             startCal = startCal.plusDays(1);
253             endCal = endCal.plusDays(1);
254         }
255         return new TimeRange(startCal.toInstant().toEpochMilli(), endCal.toInstant().toEpochMilli());
256     }
257 }