2 * Copyright (c) 2010-2023 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.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;
47 * The {@link BuienradarHandler} is responsible for handling commands, which are
48 * sent to one of the channels.
50 * @author Edwin de Jong - Initial contribution
53 public class BuienradarHandler extends BaseThingHandler {
55 private final Logger logger = LoggerFactory.getLogger(BuienradarHandler.class);
57 private final PredictionAPI client = new BuienradarPredictionAPI();
59 private @NonNullByDefault({}) ScheduledFuture<?> listenableFuture;
62 * Prevents race-condition access to listenableFuture.
64 private final Lock listenableFutureLock = new ReentrantLock();
66 private @NonNullByDefault({}) PointType location;
68 private @NonNullByDefault({}) BuienradarConfiguration config;
70 public BuienradarHandler(Thing thing) {
75 public void handleCommand(ChannelUID channelUID, Command command) {
79 public void initialize() {
80 this.config = getConfigAs(BuienradarConfiguration.class);
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");
90 location = new PointType(config.location);
91 } catch (IllegalArgumentException e) {
93 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
94 "@text/offline.conf-error-parsing-location");
99 updateStatus(ThingStatus.UNKNOWN);
102 listenableFutureLock.lock();
103 if (listenableFuture == null || listenableFuture.isCancelled()) {
104 listenableFuture = scheduler.scheduleWithFixedDelay(() -> refresh(), 0L, config.refreshIntervalMinutes,
108 listenableFutureLock.unlock();
112 private void refresh() {
113 refresh(config.retries, ZonedDateTime.now().plusMinutes(config.refreshIntervalMinutes),
114 config.exponentialBackoffRetryBaseInSeconds);
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.
123 // We are out of tries, stop retrying.
124 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
128 final Optional<List<Prediction>> predictionsOpt = client.getPredictions(location);
129 if (!predictionsOpt.isPresent()) {
130 // Did not get a result, retry the retrieval.
131 // Buienradar is not a very stable source and returns nothing quite regular
134 "Did not get a result from buienradar. Retrying. {} tries remaining, waiting {} seconds.",
135 tries, retryInSeconds);
138 "Did not get a result from buienradar. Retrying. {} tries remaining, waiting {} seconds.",
139 tries, retryInSeconds);
141 scheduler.schedule(() -> refresh(tries - 1, nextRefresh, retryInSeconds * 2), retryInSeconds,
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));
150 for (final Prediction prediction : predictions) {
151 final BigDecimal intensity = prediction.getIntensity();
153 final long minutesFromNow = prediction.getActualDateTime().until(prediction.getDateTimeOfPrediction(),
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);
160 updateState(label, new QuantityType<>(intensity, Units.MILLIMETRE_PER_HOUR));
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()));
173 public void dispose() {
175 listenableFutureLock.lock();
176 if (listenableFuture != null && !listenableFuture.isCancelled()) {
177 listenableFuture.cancel(true);
178 listenableFuture = null;
181 listenableFutureLock.unlock();