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