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