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.buienradar.internal;
15 import static java.util.concurrent.TimeUnit.MINUTES;
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;
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;
48 * The {@link BuienradarHandler} is responsible for handling commands, which are
49 * sent to one of the channels.
51 * @author Edwin de Jong - Initial contribution
54 public class BuienradarHandler extends BaseThingHandler {
56 private final Logger logger = LoggerFactory.getLogger(BuienradarHandler.class);
58 private final PredictionAPI client = new BuienradarPredictionAPI();
60 private @NonNullByDefault({}) ScheduledFuture<?> listenableFuture;
63 * Prevents race-condition access to listenableFuture.
65 private final Lock listenableFutureLock = new ReentrantLock();
67 private @NonNullByDefault({}) PointType location;
69 private @NonNullByDefault({}) BuienradarConfiguration config;
71 public BuienradarHandler(Thing thing) {
76 public void handleCommand(ChannelUID channelUID, Command command) {
80 public void initialize() {
81 this.config = getConfigAs(BuienradarConfiguration.class);
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");
91 location = new PointType(config.location);
92 } catch (IllegalArgumentException e) {
94 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
95 "@text/offline.conf-error-parsing-location");
100 updateStatus(ThingStatus.UNKNOWN);
103 listenableFutureLock.lock();
104 if (listenableFuture == null || listenableFuture.isCancelled()) {
105 listenableFuture = scheduler.scheduleWithFixedDelay(() -> refresh(), 0L, config.refreshIntervalMinutes,
109 listenableFutureLock.unlock();
113 private void refresh() {
114 refresh(config.retries, ZonedDateTime.now().plusMinutes(config.refreshIntervalMinutes),
115 config.exponentialBackoffRetryBaseInSeconds);
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.
124 // We are out of tries, stop retrying.
125 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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
135 "Did not get a result from buienradar. Retrying. {} tries remaining, waiting {} seconds.",
136 tries, retryInSeconds);
139 "Did not get a result from buienradar. Retrying. {} tries remaining, waiting {} seconds.",
140 tries, retryInSeconds);
142 scheduler.schedule(() -> refresh(tries - 1, nextRefresh, retryInSeconds * 2), retryInSeconds,
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));
151 for (final Prediction prediction : predictions) {
152 final BigDecimal intensity = prediction.getIntensity();
154 final long minutesFromNow = prediction.getActualDateTime().until(prediction.getDateTimeOfPrediction(),
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);
161 updateState(label, new QuantityType<>(intensity, Units.MILLIMETRE_PER_HOUR));
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()));
174 public void dispose() {
176 listenableFutureLock.lock();
177 if (listenableFuture != null && !listenableFuture.isCancelled()) {
178 listenableFuture.cancel(true);
179 listenableFuture = null;
182 listenableFutureLock.unlock();