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.SortedSet;
34 import java.util.concurrent.ScheduledFuture;
35 import java.util.concurrent.TimeUnit;
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;
63 * The {@link AwattarBestpriceHandler} is responsible for computing the best prices for a given configuration.
65 * @author Wolfgang Klimt - Initial contribution
68 public class AwattarBestpriceHandler extends BaseThingHandler {
69 private static final int THING_REFRESH_INTERVAL = 60;
71 private final Logger logger = LoggerFactory.getLogger(AwattarBestpriceHandler.class);
73 private @Nullable ScheduledFuture<?> thingRefresher;
75 private final TimeZoneProvider timeZoneProvider;
77 public AwattarBestpriceHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
79 this.timeZoneProvider = timeZoneProvider;
83 public void initialize() {
84 AwattarBestpriceConfiguration config = getConfigAs(AwattarBestpriceConfiguration.class);
86 if (config.length >= config.rangeDuration) {
87 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.length.value");
92 ScheduledFuture<?> localRefresher = thingRefresher;
93 if (localRefresher == null || localRefresher.isCancelled()) {
95 * The scheduler is required to run exactly at minute borders, hence we can't use scheduleWithFixedDelay
98 thingRefresher = scheduler.scheduleAtFixedRate(this::refreshChannels,
99 getMillisToNextMinute(1, timeZoneProvider), THING_REFRESH_INTERVAL * 1000,
100 TimeUnit.MILLISECONDS);
103 updateStatus(ThingStatus.UNKNOWN);
107 public void dispose() {
108 ScheduledFuture<?> localRefresher = thingRefresher;
109 if (localRefresher != null) {
110 localRefresher.cancel(true);
111 thingRefresher = null;
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());
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);
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);
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);
147 AwattarBestPriceResult result;
148 List<AwattarPrice> range = getPriceRange(bridgeHandler, timerange);
150 if (config.consecutive) {
151 range.sort(Comparator.comparing(AwattarPrice::timerange));
152 AwattarConsecutiveBestPriceResult res = new AwattarConsecutiveBestPriceResult(
153 range.subList(0, config.length), bridgeHandler.getTimeZone());
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()) {
164 range.sort(Comparator.naturalOrder());
165 AwattarNonConsecutiveBestPriceResult res = new AwattarNonConsecutiveBestPriceResult(
166 bridgeHandler.getTimeZone());
168 for (AwattarPrice price : range) {
169 res.addMember(price);
170 if (++ct >= config.length) {
176 String channelId = channelUID.getIdWithoutGroup();
180 state = OnOffType.from(result.isActive());
183 state = getDateTimeType(result.getStart(), timeZoneProvider);
186 state = getDateTimeType(result.getEnd(), timeZoneProvider);
188 case CHANNEL_COUNTDOWN:
189 diff = result.getStart() - Instant.now().toEpochMilli();
191 state = getDuration(diff);
194 case CHANNEL_REMAINING:
195 diff = result.getEnd() - Instant.now().toEpochMilli();
196 if (result.isActive()) {
197 state = getDuration(diff);
201 state = new StringType(result.getHours());
204 logger.warn("Unknown channel id {} for Thing type {}", channelUID, getThing().getThingTypeUID());
206 updateState(channelUID, state);
210 public void handleCommand(ChannelUID channelUID, Command command) {
211 if (command instanceof RefreshType) {
212 refreshChannel(channelUID);
214 logger.debug("Binding {} only supports refresh command", BINDING_ID);
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");
225 result.addAll(prices.stream().filter(x -> range.contains(x.timerange())).toList());
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);
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);
243 return new TimeRange(startCal.toInstant().toEpochMilli(), endCal.toInstant().toEpochMilli());