]> git.basschouten.com Git - openhab-addons.git/blob
0aaf824e39972c88160367fc602be640095b4821
[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.fmiweather.internal;
14
15 import static org.openhab.binding.fmiweather.internal.BindingConstants.*;
16
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;
25
26 import javax.measure.Quantity;
27 import javax.measure.Unit;
28
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.State;
48 import org.openhab.core.types.UnDefType;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 /**
53  * The {@link AbstractWeatherHandler} is responsible for handling commands, which are
54  * sent to one of the channels.
55  *
56  * @author Sami Salonen - Initial contribution
57  */
58 @NonNullByDefault
59 public abstract class AbstractWeatherHandler extends BaseThingHandler {
60
61     private static final ZoneId UTC = ZoneId.of("UTC");
62     protected static final String PROP_LONGITUDE = "longitude";
63     protected static final String PROP_LATITUDE = "latitude";
64     protected static final String PROP_NAME = "name";
65     protected static final String PROP_REGION = "region";
66     private static final long REFRESH_THROTTLE_MILLIS = 10_000;
67
68     protected static final int TIMEOUT_MILLIS = 10_000;
69     private final Logger logger = LoggerFactory.getLogger(AbstractWeatherHandler.class);
70
71     protected volatile @NonNullByDefault({}) Client client;
72     protected final AtomicReference<@Nullable ScheduledFuture<?>> futureRef = new AtomicReference<>();
73     protected volatile @Nullable FMIResponse response;
74     protected volatile int pollIntervalSeconds = 120; // reset by subclasses
75
76     private volatile long lastRefreshMillis = 0;
77     private final AtomicReference<@Nullable ScheduledFuture<?>> updateChannelsFutureRef = new AtomicReference<>();
78
79     public AbstractWeatherHandler(Thing thing) {
80         super(thing);
81     }
82
83     @Override
84     @SuppressWarnings("PMD.CompareObjectsWithEquals")
85     public void handleCommand(ChannelUID channelUID, Command command) {
86         if (RefreshType.REFRESH == command) {
87             ScheduledFuture<?> prevFuture = updateChannelsFutureRef.get();
88             ScheduledFuture<?> newFuture = updateChannelsFutureRef
89                     .updateAndGet(fut -> fut == null || fut.isDone() ? submitUpdateChannelsThrottled() : fut);
90             assert newFuture != null; // invariant
91             if (logger.isTraceEnabled()) {
92                 long delayRemainingMillis = newFuture.getDelay(TimeUnit.MILLISECONDS);
93                 if (delayRemainingMillis <= 0) {
94                     logger.trace("REFRESH received. Channels are updated");
95                 } else {
96                     logger.trace("REFRESH received. Delaying by {} ms to avoid throttle excessive REFRESH",
97                             delayRemainingMillis);
98                 }
99                 // Compare by reference to check if the future changed
100                 if (prevFuture == newFuture) {
101                     logger.trace("REFRESH received. Previous refresh ongoing, will wait for it to complete in {} ms",
102                             lastRefreshMillis + REFRESH_THROTTLE_MILLIS - System.currentTimeMillis());
103                 }
104             }
105         }
106     }
107
108     @Override
109     public void initialize() {
110         client = new Client();
111         updateStatus(ThingStatus.UNKNOWN);
112         rescheduleUpdate(0, false);
113     }
114
115     /**
116      * Call updateChannels asynchronously, possibly in a delayed fashion to throttle updates. This protects against a
117      * situation where many channels receive REFRESH command, e.g. when openHAB is requesting to update channels
118      *
119      * @return scheduled future
120      */
121     private ScheduledFuture<?> submitUpdateChannelsThrottled() {
122         long now = System.currentTimeMillis();
123         long nextRefresh = lastRefreshMillis + REFRESH_THROTTLE_MILLIS;
124         lastRefreshMillis = now;
125         if (now > nextRefresh) {
126             return scheduler.schedule(this::updateChannels, 0, TimeUnit.MILLISECONDS);
127         } else {
128             long delayMillis = nextRefresh - now;
129             return scheduler.schedule(this::updateChannels, delayMillis, TimeUnit.MILLISECONDS);
130         }
131     }
132
133     protected abstract void updateChannels();
134
135     protected abstract Request getRequest();
136
137     protected void update(int retry) {
138         if (retry < RETRIES) {
139             try {
140                 response = client.query(getRequest(), TIMEOUT_MILLIS);
141             } catch (FMIUnexpectedResponseException e) {
142                 handleError(e, retry);
143                 return;
144             } catch (FMIResponseException e) {
145                 handleError(e, retry);
146                 return;
147             }
148         } else {
149             logger.trace("Query failed. Retries exhausted, not trying again until next poll.");
150         }
151         // Update channel (if we have received a response)
152         updateChannels();
153         // Channels updated successfully or exhausted all retries. Reschedule new update
154         rescheduleUpdate(pollIntervalSeconds * 1000, false);
155     }
156
157     @Override
158     public void dispose() {
159         super.dispose();
160         response = null;
161         cancel(futureRef.getAndSet(null), true);
162         cancel(updateChannelsFutureRef.getAndSet(null), true);
163     }
164
165     protected static int lastValidIndex(Data data) {
166         if (data.values.length < 2) {
167             throw new IllegalStateException("Excepted at least two data items");
168         }
169         for (int i = data.values.length - 1; i >= 0; i--) {
170             if (data.values[i] != null) {
171                 return i;
172             }
173         }
174         // if we have reached here, it means that array was full of nulls
175         return -1;
176     }
177
178     protected static long floorToEvenMinutes(long epochSeconds, int roundMinutes) {
179         long roundSecs = roundMinutes * 60;
180         return (epochSeconds / roundSecs) * roundSecs;
181     }
182
183     protected static long ceilToEvenMinutes(long epochSeconds, int roundMinutes) {
184         double epochDouble = epochSeconds;
185         long roundSecs = roundMinutes * 60;
186         double roundSecsDouble = (roundMinutes * 60);
187         return (long) Math.ceil(epochDouble / roundSecsDouble) * roundSecs;
188     }
189
190     /**
191      * Update QuantityType channel state
192      *
193      * @param channelUID channel UID
194      * @param epochSecond value to update
195      */
196     protected <T extends Quantity<T>> void updateEpochSecondStateIfLinked(ChannelUID channelUID, long epochSecond) {
197         if (isLinked(channelUID)) {
198             updateState(channelUID, new DateTimeType(ZonedDateTime.ofInstant(Instant.ofEpochSecond(epochSecond), UTC)
199                     .withZoneSameInstant(ZoneId.systemDefault())));
200         }
201     }
202
203     /**
204      * Update QuantityType or DecimalType channel state
205      *
206      * Updates UNDEF state when value is null
207      *
208      * @param channelUID channel UID
209      * @param value value to update
210      * @param unit unit associated with the value
211      */
212     protected void updateStateIfLinked(ChannelUID channelUID, @Nullable BigDecimal value, @Nullable Unit<?> unit) {
213         if (isLinked(channelUID)) {
214             updateState(channelUID, getState(value, unit));
215         }
216     }
217
218     /**
219      * Return QuantityType or DecimalType channel state
220      *
221      * @param value value to update
222      * @param unit unit associated with the value
223      * @return UNDEF state when value is null, otherwise QuantityType or DecimalType
224      */
225     protected State getState(@Nullable BigDecimal value, @Nullable Unit<?> unit) {
226         if (value == null) {
227             return UnDefType.UNDEF;
228         } else if (unit == null) {
229             return new DecimalType(value);
230         } else {
231             return new QuantityType<>(value, unit);
232         }
233     }
234
235     /**
236      * Unwrap optional value and log with ERROR if value is not present
237      *
238      * This should be used only when we expect value to be present, and the reason for missing value corresponds to
239      * description of {@link FMIUnexpectedResponseException}.
240      *
241      * @param optional optional to unwrap
242      * @param messageIfNotPresent logging message
243      * @param args arguments to logging
244      * @throws FMIUnexpectedResponseException when value is not present
245      * @return unwrapped value of the optional
246      */
247     protected <T> T unwrap(Optional<T> optional, String messageIfNotPresent, Object... args)
248             throws FMIUnexpectedResponseException {
249         if (optional.isPresent()) {
250             return optional.get();
251         } else {
252             // logger.error(messageIfNotPresent, args) avoided due to static analyzer
253             String formattedMessage = String.format(messageIfNotPresent, args);
254             throw new FMIUnexpectedResponseException(formattedMessage);
255         }
256     }
257
258     protected void handleError(FMIResponseException e, int retry) {
259         response = null;
260         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
261                 String.format("%s: %s", e.getClass().getSimpleName(), e.getMessage()));
262         logger.trace("Query failed. Increase retry count {} and try again. Error: {} {}", retry, e.getClass().getName(),
263                 e.getMessage());
264         // Try again, with increased retry count
265         rescheduleUpdate(RETRY_DELAY_MILLIS, false, retry + 1);
266     }
267
268     protected void rescheduleUpdate(long delayMillis, boolean mayInterruptIfRunning) {
269         rescheduleUpdate(delayMillis, mayInterruptIfRunning, 0);
270     }
271
272     protected void rescheduleUpdate(long delayMillis, boolean mayInterruptIfRunning, int retry) {
273         cancel(futureRef.getAndSet(scheduler.schedule(() -> this.update(retry), delayMillis, TimeUnit.MILLISECONDS)),
274                 mayInterruptIfRunning);
275     }
276
277     private static void cancel(@Nullable ScheduledFuture<?> future, boolean mayInterruptIfRunning) {
278         if (future != null) {
279             future.cancel(mayInterruptIfRunning);
280         }
281     }
282 }