2 * Copyright (c) 2010-2020 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) {
79 @SuppressWarnings("null")
81 public void initialize() {
82 this.config = getConfigAs(BuienradarConfiguration.class);
84 boolean configValid = true;
85 if (StringUtils.trimToNull(config.location) == null) {
86 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
87 "@text/offline.conf-error-missing-location");
92 location = new PointType(config.location);
93 } catch (IllegalArgumentException e) {
95 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
96 "@text/offline.conf-error-parsing-location");
101 updateStatus(ThingStatus.UNKNOWN);
104 listenableFutureLock.lock();
105 if (listenableFuture == null || listenableFuture.isCancelled()) {
106 listenableFuture = scheduler.scheduleWithFixedDelay(() -> refresh(), 0L, config.refreshIntervalMinutes,
110 listenableFutureLock.unlock();
114 private void refresh() {
115 refresh(config.retries, ZonedDateTime.now().plusMinutes(config.refreshIntervalMinutes),
116 config.exponentialBackoffRetryBaseInSeconds);
119 private void refresh(int tries, ZonedDateTime nextRefresh, int retryInSeconds) {
120 if (nextRefresh.isBefore(ZonedDateTime.now())) {
121 // The next refresh is already running, stop retries.
125 // We are out of tries, stop retrying.
126 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
130 @SuppressWarnings("null")
131 final Optional<List<Prediction>> predictionsOpt = client.getPredictions(location);
132 if (!predictionsOpt.isPresent()) {
133 // Did not get a result, retry the retrieval.
134 logger.warn("Did not get a result from buienradar. Retrying. {} tries remaining, waiting {} seconds.",
135 tries, retryInSeconds);
136 scheduler.schedule(() -> refresh(tries - 1, nextRefresh, retryInSeconds * 2), retryInSeconds,
140 final List<Prediction> predictions = predictionsOpt.get();
141 if (!predictions.isEmpty()) {
142 final ZonedDateTime actual = predictions.get(0).getActualDateTime();
143 updateState(BuienradarBindingConstants.ACTUAL_DATETIME, new DateTimeType(actual));
145 for (final Prediction prediction : predictions) {
146 final BigDecimal intensity = prediction.getIntensity();
148 final long minutesFromNow = prediction.getActualDateTime().until(prediction.getDateTimeOfPrediction(),
150 logger.debug("Forecast for {} at {} made at {} is {}", minutesFromNow,
151 prediction.getDateTimeOfPrediction(), prediction.getActualDateTime(), intensity);
152 if (minutesFromNow >= 0 && minutesFromNow <= 115) {
153 final String label = String.format(Locale.ENGLISH, "forecast_%d", minutesFromNow);
155 updateState(label, new QuantityType<>(intensity, Units.MILLIMETRE_PER_HOUR));
159 updateStatus(ThingStatus.ONLINE);
160 } catch (IOException e) {
161 logger.warn("Cannot retrieve predictions", e);
162 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.COMMUNICATION_ERROR,
163 String.format("Could not reach buienradar: %s", e.getMessage()));
167 @SuppressWarnings("null")
169 public void dispose() {
171 listenableFutureLock.lock();
172 if (listenableFuture != null && !listenableFuture.isCancelled()) {
173 listenableFuture.cancel(true);
174 listenableFuture = null;
177 listenableFutureLock.unlock();