]> git.basschouten.com Git - openhab-addons.git/blob
025e675bc884ee4f97f5515dcab0c3f8079d29a3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.buienradar.internal;
14
15 import static java.util.concurrent.TimeUnit.MINUTES;
16
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.time.ZonedDateTime;
20 import java.time.temporal.ChronoUnit;
21 import java.util.List;
22 import java.util.Locale;
23 import java.util.Optional;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.locks.Lock;
27 import java.util.concurrent.locks.ReentrantLock;
28
29 import org.apache.commons.lang.StringUtils;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.openhab.binding.buienradar.internal.buienradarapi.BuienradarPredictionAPI;
32 import org.openhab.binding.buienradar.internal.buienradarapi.Prediction;
33 import org.openhab.binding.buienradar.internal.buienradarapi.PredictionAPI;
34 import org.openhab.core.library.types.DateTimeType;
35 import org.openhab.core.library.types.PointType;
36 import org.openhab.core.library.types.QuantityType;
37 import org.openhab.core.library.unit.Units;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.types.Command;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 /**
48  * The {@link BuienradarHandler} is responsible for handling commands, which are
49  * sent to one of the channels.
50  *
51  * @author Edwin de Jong - Initial contribution
52  */
53 @NonNullByDefault
54 public class BuienradarHandler extends BaseThingHandler {
55
56     private final Logger logger = LoggerFactory.getLogger(BuienradarHandler.class);
57
58     private final PredictionAPI client = new BuienradarPredictionAPI();
59
60     private @NonNullByDefault({}) ScheduledFuture<?> listenableFuture;
61
62     /**
63      * Prevents race-condition access to listenableFuture.
64      */
65     private final Lock listenableFutureLock = new ReentrantLock();
66
67     private @NonNullByDefault({}) PointType location;
68
69     private @NonNullByDefault({}) BuienradarConfiguration config;
70
71     public BuienradarHandler(Thing thing) {
72         super(thing);
73     }
74
75     @Override
76     public void handleCommand(ChannelUID channelUID, Command command) {
77     }
78
79     @Override
80     public void initialize() {
81         this.config = getConfigAs(BuienradarConfiguration.class);
82
83         boolean configValid = true;
84         if (StringUtils.trimToNull(config.location) == null) {
85             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
86                     "@text/offline.conf-error-missing-location");
87             configValid = false;
88         }
89
90         try {
91             location = new PointType(config.location);
92         } catch (IllegalArgumentException e) {
93             location = null;
94             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
95                     "@text/offline.conf-error-parsing-location");
96             configValid = false;
97         }
98
99         if (configValid) {
100             updateStatus(ThingStatus.UNKNOWN);
101         }
102         try {
103             listenableFutureLock.lock();
104             if (listenableFuture == null || listenableFuture.isCancelled()) {
105                 listenableFuture = scheduler.scheduleWithFixedDelay(() -> refresh(), 0L, config.refreshIntervalMinutes,
106                         MINUTES);
107             }
108         } finally {
109             listenableFutureLock.unlock();
110         }
111     }
112
113     private void refresh() {
114         refresh(config.retries, ZonedDateTime.now().plusMinutes(config.refreshIntervalMinutes),
115                 config.exponentialBackoffRetryBaseInSeconds);
116     }
117
118     private void refresh(int tries, ZonedDateTime nextRefresh, int retryInSeconds) {
119         if (nextRefresh.isBefore(ZonedDateTime.now())) {
120             // The next refresh is already running, stop retries.
121             return;
122         }
123         if (tries <= 0) {
124             // We are out of tries, stop retrying.
125             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
126             return;
127         }
128         try {
129             final Optional<List<Prediction>> predictionsOpt = client.getPredictions(location);
130             if (!predictionsOpt.isPresent()) {
131                 // Did not get a result, retry the retrieval.
132                 // Buienradar is not a very stable source and returns nothing quite regular
133                 if (tries <= 2) {
134                     logger.warn(
135                             "Did not get a result from buienradar. Retrying. {} tries remaining, waiting {} seconds.",
136                             tries, retryInSeconds);
137                 } else {
138                     logger.debug(
139                             "Did not get a result from buienradar. Retrying. {} tries remaining, waiting {} seconds.",
140                             tries, retryInSeconds);
141                 }
142                 scheduler.schedule(() -> refresh(tries - 1, nextRefresh, retryInSeconds * 2), retryInSeconds,
143                         TimeUnit.SECONDS);
144                 return;
145             }
146             final List<Prediction> predictions = predictionsOpt.get();
147             if (!predictions.isEmpty()) {
148                 final ZonedDateTime actual = predictions.get(0).getActualDateTime();
149                 updateState(BuienradarBindingConstants.ACTUAL_DATETIME, new DateTimeType(actual));
150             }
151             for (final Prediction prediction : predictions) {
152                 final BigDecimal intensity = prediction.getIntensity();
153
154                 final long minutesFromNow = prediction.getActualDateTime().until(prediction.getDateTimeOfPrediction(),
155                         ChronoUnit.MINUTES);
156                 logger.debug("Forecast for {} at {} made at {} is {}", minutesFromNow,
157                         prediction.getDateTimeOfPrediction(), prediction.getActualDateTime(), intensity);
158                 if (minutesFromNow >= 0 && minutesFromNow <= 115) {
159                     final String label = String.format(Locale.ENGLISH, "forecast_%d", minutesFromNow);
160
161                     updateState(label, new QuantityType<>(intensity, Units.MILLIMETRE_PER_HOUR));
162                 }
163             }
164
165             updateStatus(ThingStatus.ONLINE);
166         } catch (IOException e) {
167             logger.warn("Cannot retrieve predictions", e);
168             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.COMMUNICATION_ERROR,
169                     String.format("Could not reach buienradar: %s", e.getMessage()));
170         }
171     }
172
173     @Override
174     public void dispose() {
175         try {
176             listenableFutureLock.lock();
177             if (listenableFuture != null && !listenableFuture.isCancelled()) {
178                 listenableFuture.cancel(true);
179                 listenableFuture = null;
180             }
181         } finally {
182             listenableFutureLock.unlock();
183         }
184     }
185 }