2 * Copyright (c) 2010-2021 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.fmiweather.internal;
15 import static org.openhab.binding.fmiweather.internal.BindingConstants.*;
17 import java.math.BigDecimal;
18 import java.time.Instant;
19 import java.time.ZoneId;
20 import java.time.ZonedDateTime;
21 import java.util.Optional;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.atomic.AtomicReference;
26 import javax.measure.Quantity;
27 import javax.measure.Unit;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.fmiweather.internal.client.Client;
32 import org.openhab.binding.fmiweather.internal.client.Data;
33 import org.openhab.binding.fmiweather.internal.client.FMIResponse;
34 import org.openhab.binding.fmiweather.internal.client.Request;
35 import org.openhab.binding.fmiweather.internal.client.exception.FMIResponseException;
36 import org.openhab.binding.fmiweather.internal.client.exception.FMIUnexpectedResponseException;
37 import org.openhab.core.library.types.DateTimeType;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.Thing;
42 import org.openhab.core.thing.ThingStatus;
43 import org.openhab.core.thing.ThingStatusDetail;
44 import org.openhab.core.thing.binding.BaseThingHandler;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.openhab.core.types.UnDefType;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * The {@link AbstractWeatherHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author Sami Salonen - Initial contribution
58 public abstract class AbstractWeatherHandler extends BaseThingHandler {
60 private static final ZoneId UTC = ZoneId.of("UTC");
61 protected static final String PROP_LONGITUDE = "longitude";
62 protected static final String PROP_LATITUDE = "latitude";
63 protected static final String PROP_NAME = "name";
64 protected static final String PROP_REGION = "region";
65 private static final long REFRESH_THROTTLE_MILLIS = 10_000;
67 protected static final int TIMEOUT_MILLIS = 10_000;
68 private final Logger logger = LoggerFactory.getLogger(AbstractWeatherHandler.class);
70 protected volatile @NonNullByDefault({}) Client client;
71 protected final AtomicReference<@Nullable ScheduledFuture<?>> futureRef = new AtomicReference<>();
72 protected volatile @Nullable FMIResponse response;
73 protected volatile int pollIntervalSeconds = 120; // reset by subclasses
75 private volatile long lastRefreshMillis = 0;
76 private final AtomicReference<@Nullable ScheduledFuture<?>> updateChannelsFutureRef = new AtomicReference<>();
78 public AbstractWeatherHandler(Thing thing) {
83 @SuppressWarnings("PMD.CompareObjectsWithEquals")
84 public void handleCommand(ChannelUID channelUID, Command command) {
85 if (RefreshType.REFRESH == command) {
86 ScheduledFuture<?> prevFuture = updateChannelsFutureRef.get();
87 ScheduledFuture<?> newFuture = updateChannelsFutureRef
88 .updateAndGet(fut -> fut == null || fut.isDone() ? submitUpdateChannelsThrottled() : fut);
89 assert newFuture != null; // invariant
90 if (logger.isTraceEnabled()) {
91 long delayRemainingMillis = newFuture.getDelay(TimeUnit.MILLISECONDS);
92 if (delayRemainingMillis <= 0) {
93 logger.trace("REFRESH received. Channels are updated");
95 logger.trace("REFRESH received. Delaying by {} ms to avoid throttle excessive REFRESH",
96 delayRemainingMillis);
98 // Compare by reference to check if the future changed
99 if (prevFuture == newFuture) {
100 logger.trace("REFRESH received. Previous refresh ongoing, will wait for it to complete in {} ms",
101 lastRefreshMillis + REFRESH_THROTTLE_MILLIS - System.currentTimeMillis());
108 public void initialize() {
109 client = new Client();
110 updateStatus(ThingStatus.UNKNOWN);
111 rescheduleUpdate(0, false);
115 * Call updateChannels asynchronously, possibly in a delayed fashion to throttle updates. This protects against a
116 * situation where many channels receive REFRESH command, e.g. when openHAB is requesting to update channels
118 * @return scheduled future
120 private ScheduledFuture<?> submitUpdateChannelsThrottled() {
121 long now = System.currentTimeMillis();
122 long nextRefresh = lastRefreshMillis + REFRESH_THROTTLE_MILLIS;
123 lastRefreshMillis = now;
124 if (now > nextRefresh) {
125 return scheduler.schedule(this::updateChannels, 0, TimeUnit.MILLISECONDS);
127 long delayMillis = nextRefresh - now;
128 return scheduler.schedule(this::updateChannels, delayMillis, TimeUnit.MILLISECONDS);
132 protected abstract void updateChannels();
134 protected abstract Request getRequest();
136 protected void update(int retry) {
137 if (retry < RETRIES) {
139 response = client.query(getRequest(), TIMEOUT_MILLIS);
140 } catch (FMIUnexpectedResponseException e) {
141 handleError(e, retry);
143 } catch (FMIResponseException e) {
144 handleError(e, retry);
148 logger.trace("Query failed. Retries exhausted, not trying again until next poll.");
150 // Update channel (if we have received a response)
152 // Channels updated successfully or exhausted all retries. Reschedule new update
153 rescheduleUpdate(pollIntervalSeconds * 1000, false);
157 public void dispose() {
160 cancel(futureRef.getAndSet(null), true);
161 cancel(updateChannelsFutureRef.getAndSet(null), true);
164 protected static int lastValidIndex(Data data) {
165 if (data.values.length < 2) {
166 throw new IllegalStateException("Excepted at least two data items");
168 if (data.values[0] == null) {
171 for (int i = 1; i < data.values.length; i++) {
172 if (data.values[i] == null) {
176 if (data.values[data.values.length - 1] == null) {
179 return data.values.length - 1;
182 protected static long floorToEvenMinutes(long epochSeconds, int roundMinutes) {
183 long roundSecs = roundMinutes * 60;
184 return (epochSeconds / roundSecs) * roundSecs;
187 protected static long ceilToEvenMinutes(long epochSeconds, int roundMinutes) {
188 double epochDouble = epochSeconds;
189 long roundSecs = roundMinutes * 60;
190 double roundSecsDouble = (roundMinutes * 60);
191 return (long) Math.ceil(epochDouble / roundSecsDouble) * roundSecs;
195 * Update QuantityType channel state
197 * @param channelUID channel UID
198 * @param epochSecond value to update
199 * @param unit unit associated with the value
201 protected <T extends Quantity<T>> void updateEpochSecondStateIfLinked(ChannelUID channelUID, long epochSecond) {
202 if (isLinked(channelUID)) {
203 updateState(channelUID, new DateTimeType(ZonedDateTime.ofInstant(Instant.ofEpochSecond(epochSecond), UTC)
204 .withZoneSameInstant(ZoneId.systemDefault())));
209 * Update QuantityType or DecimalType channel state
211 * Updates UNDEF state when value is null
213 * @param channelUID channel UID
214 * @param value value to update
215 * @param unit unit associated with the value
217 protected void updateStateIfLinked(ChannelUID channelUID, @Nullable BigDecimal value, @Nullable Unit<?> unit) {
218 if (isLinked(channelUID)) {
220 updateState(channelUID, UnDefType.UNDEF);
221 } else if (unit == null) {
222 updateState(channelUID, new DecimalType(value));
224 updateState(channelUID, new QuantityType<>(value, unit));
230 * Unwrap optional value and log with ERROR if value is not present
232 * This should be used only when we expect value to be present, and the reason for missing value corresponds to
233 * description of {@link FMIUnexpectedResponseException}.
235 * @param optional optional to unwrap
236 * @param messageIfNotPresent logging message
237 * @param args arguments to logging
238 * @throws FMIUnexpectedResponseException when value is not present
239 * @return unwrapped value of the optional
241 protected <T> T unwrap(Optional<T> optional, String messageIfNotPresent, Object... args)
242 throws FMIUnexpectedResponseException {
243 if (optional.isPresent()) {
244 return optional.get();
246 // logger.error(messageIfNotPresent, args) avoided due to static analyzer
247 String formattedMessage = String.format(messageIfNotPresent, args);
248 throw new FMIUnexpectedResponseException(formattedMessage);
252 protected void handleError(FMIResponseException e, int retry) {
254 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
255 String.format("%s: %s", e.getClass().getSimpleName(), e.getMessage()));
256 logger.trace("Query failed. Increase retry count {} and try again. Error: {} {}", retry, e.getClass().getName(),
258 // Try again, with increased retry count
259 rescheduleUpdate(RETRY_DELAY_MILLIS, false, retry + 1);
262 protected void rescheduleUpdate(long delayMillis, boolean mayInterruptIfRunning) {
263 rescheduleUpdate(delayMillis, mayInterruptIfRunning, 0);
266 protected void rescheduleUpdate(long delayMillis, boolean mayInterruptIfRunning, int retry) {
267 cancel(futureRef.getAndSet(scheduler.schedule(() -> this.update(retry), delayMillis, TimeUnit.MILLISECONDS)),
268 mayInterruptIfRunning);
271 private static void cancel(@Nullable ScheduledFuture<?> future, boolean mayInterruptIfRunning) {
272 if (future != null) {
273 future.cancel(mayInterruptIfRunning);