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.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;
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;
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 {
71 private final Logger logger = LoggerFactory.getLogger(AwattarBestpriceHandler.class);
73 private final int thingRefreshInterval = 60;
75 private ScheduledFuture<?> thingRefresher;
77 private final TimeZoneProvider timeZoneProvider;
79 public AwattarBestpriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
81 this.timeZoneProvider = timeZoneProvider;
85 public void initialize() {
86 AwattarBestpriceConfiguration config = getConfigAs(AwattarBestpriceConfiguration.class);
88 boolean configValid = true;
90 if (config.length >= config.rangeDuration) {
91 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.length.value");
100 ScheduledFuture<?> localRefresher = thingRefresher;
101 if (localRefresher == null || localRefresher.isCancelled()) {
103 * The scheduler is required to run exactly at minute borders, hence we can't use scheduleWithFixedDelay
106 thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
107 getMillisToNextMinute(1, timeZoneProvider), thingRefreshInterval * 1000, TimeUnit.MILLISECONDS);
110 updateStatus(ThingStatus.UNKNOWN);
114 public void dispose() {
115 ScheduledFuture<?> localRefresher = thingRefresher;
116 if (localRefresher != null) {
117 localRefresher.cancel(true);
118 thingRefresher = null;
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());
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);
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);
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);
154 AwattarBestPriceResult result;
155 if (config.consecutive) {
156 ArrayList<AwattarPrice> range = new ArrayList<>(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());
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()) {
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());
176 for (AwattarPrice price : range) {
177 res.addMember(price);
178 if (++ct >= config.length) {
184 String channelId = channelUID.getIdWithoutGroup();
188 state = OnOffType.from(result.isActive());
191 state = getDateTimeType(result.getStart(), timeZoneProvider);
194 state = getDateTimeType(result.getEnd(), timeZoneProvider);
196 case CHANNEL_COUNTDOWN:
197 diff = result.getStart() - Instant.now().toEpochMilli();
199 state = getDuration(diff);
202 case CHANNEL_REMAINING:
203 diff = result.getEnd() - Instant.now().toEpochMilli();
204 if (result.isActive()) {
205 state = getDuration(diff);
209 state = new StringType(result.getHours());
212 logger.warn("Unknown channel id {} for Thing type {}", channelUID, getThing().getThingTypeUID());
214 updateState(channelUID, state);
218 public void handleCommand(ChannelUID channelUID, Command command) {
219 if (command instanceof RefreshType) {
220 refreshChannel(channelUID);
222 logger.debug("Binding {} only supports refresh command", BINDING_ID);
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");
234 result.addAll(priceMap.values().stream().filter(x -> x.isBetween(range.start, range.end))
235 .collect(Collectors.toSet()));
236 result.sort(comparator);
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);
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);
254 return new Timerange(startCal.toInstant().toEpochMilli(), endCal.toInstant().toEpochMilli());
257 private class Timerange {
261 Timerange(long start, long end) {