]> git.basschouten.com Git - openhab-addons.git/blob
11e09b5a272c9c7a932c7a8f5b9225cc438c9670
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.Comparator;
32 import java.util.List;
33 import java.util.SortedMap;
34 import java.util.concurrent.ScheduledFuture;
35 import java.util.concurrent.TimeUnit;
36 import java.util.stream.Collectors;
37
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.awattar.internal.AwattarBestPriceResult;
41 import org.openhab.binding.awattar.internal.AwattarBestpriceConfiguration;
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;
62
63 /**
64  * The {@link AwattarBestpriceHandler} is responsible for computing the best prices for a given configuration.
65  *
66  * @author Wolfgang Klimt - Initial contribution
67  */
68 @NonNullByDefault
69 public class AwattarBestpriceHandler extends BaseThingHandler {
70
71     private final Logger logger = LoggerFactory.getLogger(AwattarBestpriceHandler.class);
72
73     private final int thingRefreshInterval = 60;
74     @Nullable
75     private ScheduledFuture<?> thingRefresher;
76
77     private final TimeZoneProvider timeZoneProvider;
78
79     public AwattarBestpriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
80         super(thing);
81         this.timeZoneProvider = timeZoneProvider;
82     }
83
84     @Override
85     public void initialize() {
86         AwattarBestpriceConfiguration config = getConfigAs(AwattarBestpriceConfiguration.class);
87
88         boolean configValid = true;
89
90         if (config.length >= config.rangeDuration) {
91             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.length.value");
92             configValid = false;
93         }
94
95         if (!configValid) {
96             return;
97         }
98
99         synchronized (this) {
100             ScheduledFuture<?> localRefresher = thingRefresher;
101             if (localRefresher == null || localRefresher.isCancelled()) {
102                 /*
103                  * The scheduler is required to run exactly at minute borders, hence we can't use scheduleWithFixedDelay
104                  * here
105                  */
106                 thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
107                         getMillisToNextMinute(1, timeZoneProvider), thingRefreshInterval * 1000, TimeUnit.MILLISECONDS);
108             }
109         }
110         updateStatus(ThingStatus.UNKNOWN);
111     }
112
113     @Override
114     public void dispose() {
115         ScheduledFuture<?> localRefresher = thingRefresher;
116         if (localRefresher != null) {
117             localRefresher.cancel(true);
118             thingRefresher = null;
119         }
120     }
121
122     public void refreshChannels() {
123         updateStatus(ThingStatus.ONLINE);
124         for (Channel channel : getThing().getChannels()) {
125             ChannelUID channelUID = channel.getUID();
126             if (ChannelKind.STATE.equals(channel.getKind()) && isLinked(channelUID)) {
127                 refreshChannel(channel.getUID());
128             }
129         }
130     }
131
132     public void refreshChannel(ChannelUID channelUID) {
133         State state = UnDefType.UNDEF;
134         Bridge bridge = getBridge();
135         if (bridge == null) {
136             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.bridge.missing");
137             updateState(channelUID, state);
138             return;
139         }
140         AwattarBridgeHandler bridgeHandler = (AwattarBridgeHandler) bridge.getHandler();
141         if (bridgeHandler == null || bridgeHandler.getPriceMap() == null) {
142             logger.debug("No prices available, so can't refresh channel.");
143             // no prices available, can't continue
144             updateState(channelUID, state);
145             return;
146         }
147         AwattarBestpriceConfiguration config = getConfigAs(AwattarBestpriceConfiguration.class);
148         Timerange timerange = getRange(config.rangeStart, config.rangeDuration, bridgeHandler.getTimeZone());
149         if (!(bridgeHandler.containsPriceFor(timerange.start) && bridgeHandler.containsPriceFor(timerange.end))) {
150             updateState(channelUID, state);
151             return;
152         }
153
154         AwattarBestPriceResult result;
155         if (config.consecutive) {
156             ArrayList<AwattarPrice> range = new ArrayList<AwattarPrice>(config.rangeDuration);
157             range.addAll(getPriceRange(bridgeHandler, timerange,
158                     (o1, o2) -> Long.compare(o1.getStartTimestamp(), o2.getStartTimestamp())));
159             AwattarConsecutiveBestPriceResult res = new AwattarConsecutiveBestPriceResult(
160                     range.subList(0, config.length), bridgeHandler.getTimeZone());
161
162             for (int i = 1; i <= range.size() - config.length; i++) {
163                 AwattarConsecutiveBestPriceResult res2 = new AwattarConsecutiveBestPriceResult(
164                         range.subList(i, i + config.length), bridgeHandler.getTimeZone());
165                 if (res2.getPriceSum() < res.getPriceSum()) {
166                     res = res2;
167                 }
168             }
169             result = res;
170         } else {
171             List<AwattarPrice> range = getPriceRange(bridgeHandler, timerange,
172                     (o1, o2) -> Double.compare(o1.getPrice(), o2.getPrice()));
173             AwattarNonConsecutiveBestPriceResult res = new AwattarNonConsecutiveBestPriceResult(config.length,
174                     bridgeHandler.getTimeZone());
175             int ct = 0;
176             for (AwattarPrice price : range) {
177                 res.addMember(price);
178                 if (++ct >= config.length) {
179                     break;
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                 }
201                 break;
202             case CHANNEL_REMAINING:
203                 diff = result.getEnd() - Instant.now().toEpochMilli();
204                 if (result.isActive()) {
205                     state = getDuration(diff);
206                 }
207                 break;
208             case CHANNEL_HOURS:
209                 state = new StringType(result.getHours());
210                 break;
211             default:
212                 logger.warn("Unknown channel id {} for Thing type {}", channelUID, getThing().getThingTypeUID());
213         }
214         updateState(channelUID, state);
215     }
216
217     @Override
218     public void handleCommand(ChannelUID channelUID, Command command) {
219         if (command instanceof RefreshType) {
220             refreshChannel(channelUID);
221         } else {
222             logger.debug("Binding {} only supports refresh command", BINDING_ID);
223         }
224     }
225
226     private List<AwattarPrice> getPriceRange(AwattarBridgeHandler bridgeHandler, Timerange range,
227             Comparator<AwattarPrice> comparator) {
228         ArrayList<AwattarPrice> result = new ArrayList<>();
229         SortedMap<Long, AwattarPrice> priceMap = bridgeHandler.getPriceMap();
230         if (priceMap == null) {
231             logger.debug("No prices available, can't compute ranges");
232             return result;
233         }
234         result.addAll(priceMap.values().stream().filter(x -> x.isBetween(range.start, range.end))
235                 .collect(Collectors.toSet()));
236         result.sort(comparator);
237         return result;
238     }
239
240     private Timerange getRange(int start, int duration, ZoneId zoneId) {
241         ZonedDateTime startCal = getCalendarForHour(start, zoneId);
242         ZonedDateTime endCal = startCal.plusHours(duration);
243         ZonedDateTime now = ZonedDateTime.now(zoneId);
244         if (now.getHour() < start) {
245             // we are before the range, so we might be still within the last range
246             startCal = startCal.minusDays(1);
247             endCal = endCal.minusDays(1);
248         }
249         if (endCal.toInstant().toEpochMilli() < Instant.now().toEpochMilli()) {
250             // span is in the past, add one day
251             startCal = startCal.plusDays(1);
252             endCal = endCal.plusDays(1);
253         }
254         return new Timerange(startCal.toInstant().toEpochMilli(), endCal.toInstant().toEpochMilli());
255     }
256
257     private class Timerange {
258         long start;
259         long end;
260
261         Timerange(long start, long end) {
262             this.start = start;
263             this.end = end;
264         }
265     }
266 }