2 * Copyright (c) 2010-2022 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.buienradarapi;
15 import java.io.IOException;
16 import java.math.BigDecimal;
17 import java.math.RoundingMode;
18 import java.time.LocalDate;
19 import java.time.LocalTime;
20 import java.time.ZoneId;
21 import java.time.ZonedDateTime;
22 import java.util.ArrayList;
23 import java.util.LinkedList;
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.Optional;
27 import java.util.stream.Collectors;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.openhab.core.io.net.http.HttpUtil;
31 import org.openhab.core.library.types.PointType;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * The {@link BuienradarPredictionAPI} class implements the methods for retrieving results from the buienradar.nl
39 * @author Edwin de Jong - Initial contribution
42 public class BuienradarPredictionAPI implements PredictionAPI {
43 private static final ZoneId ZONE_AMSTERDAM = ZoneId.of("Europe/Amsterdam");
45 private static final String BASE_ADDRESS = "https://gpsgadget.buienradar.nl/data/raintext";
47 private static final int TIMEOUT_MS = 3000;
49 private final Logger logger = LoggerFactory.getLogger(BuienradarPredictionAPI.class);
52 * Parses a raw intensity string, such as <code>000</code>, into the right intensity in mm / hour.
54 * @param intensityStr The raw intensity string, being 3 characters long.
55 * @return The real intensity in mm / hour
56 * @throws BuienradarParseException when the intensity string could not be parsed.
58 public static BigDecimal parseIntensity(String intensityStr) throws BuienradarParseException {
60 // Intensity in mm / hour = 10^((value-109)/32)
61 double unrounded = Math.pow(10.0, (Integer.parseInt(intensityStr) - 109) / 32.0);
62 return BigDecimal.valueOf(unrounded).setScale(2, RoundingMode.HALF_EVEN);
63 } catch (NumberFormatException e) {
64 throw new BuienradarParseException("Could not parse intensity part of API", e);
69 * Parses a time string, such as <code>10:30</code> into a ZonedDateTime, using the reference ZonedDateTime to find
73 * @param timeStr The time string to parse.
74 * @param now The reference time to use.
75 * @return A ZonedDateTime of the indicated time.
76 * @throws BuienradarParseException When the time string cannot be correctly parsed.
78 public static ZonedDateTime parseDateTime(String timeStr, ZonedDateTime now) throws BuienradarParseException {
79 final String[] timeElements = timeStr.split(":");
80 if (timeElements.length != 2) {
81 throw new BuienradarParseException("Expecting exactly two time elements");
84 final int hour = Integer.parseInt(timeElements[0]);
85 final int minute = Integer.parseInt(timeElements[1]);
86 final LocalTime time = LocalTime.of(hour, minute);
88 final LocalDate localDateInAmsterdam = now.withZoneSameInstant(ZONE_AMSTERDAM).toLocalDate();
90 final ZonedDateTime tryDateTime = time.atDate(localDateInAmsterdam).atZone(ZONE_AMSTERDAM);
91 if (tryDateTime.plusMinutes(20).isBefore(now)) {
92 // Check me: could this go wrong at DTS days?
93 return time.atDate(localDateInAmsterdam.plusDays(1)).atZone(ZONE_AMSTERDAM);
100 * Parses a line returned from the buienradar API service. An example line could be <code>100|23:00</code>.
102 * @param line The line to parse, such as <code>100|23:00</code>
103 * @param now The reference time to determine which instant to match to.
104 * @param actual The date time of the 'actual' prediction if known. If None is given, it is assumed this is the
105 * first row of the results.
106 * @return A Prediction interface, which contains the tuple with the intensity and the time.
107 * @throws BuienradarParseException Thrown when the line could not be correctly parsed.
109 public static Prediction parseLine(String line, ZonedDateTime now, Optional<ZonedDateTime> actual)
110 throws BuienradarParseException {
111 final String[] lineElements = line.trim().split("\\|");
112 if (lineElements.length != 2) {
113 throw new BuienradarParseException(
114 String.format("Expected two line elements, but found %s", lineElements.length));
116 final BigDecimal intensityOut = parseIntensity(lineElements[0]);
117 final ZonedDateTime dateTime = parseDateTime(lineElements[1], now);
118 return new Prediction() {
121 public final BigDecimal getIntensity() {
126 public ZonedDateTime getDateTimeOfPrediction() {
131 public ZonedDateTime getActualDateTime() {
132 return actual.orElseGet(this::getDateTimeOfPrediction);
138 public Optional<List<Prediction>> getPredictions(PointType location) throws IOException {
139 final String address = String.format(Locale.ENGLISH, BASE_ADDRESS + "?lat=%.2f&lon=%.2f",
140 location.getLatitude().doubleValue(), location.getLongitude().doubleValue());
144 result = HttpUtil.executeUrl("GET", address, TIMEOUT_MS);
145 } catch (IOException e) {
146 logger.debug("IO Exception when trying to retrieve Buienradar results: {}", e.getMessage());
147 return Optional.empty();
150 if (result.trim().isEmpty()) {
151 logger.warn("Buienradar API at URI {} return empty result", address);
152 return Optional.empty();
154 final List<Prediction> predictions = new ArrayList<>(24);
155 final List<String> errors = new LinkedList<>();
156 logger.debug("Returned result from buienradar: {}", result);
157 final String[] lines = result.split("\n");
158 Optional<ZonedDateTime> actual = Optional.empty();
159 for (String line : lines) {
161 final Prediction prediction = parseLine(line, ZonedDateTime.now(), actual);
162 actual = Optional.of(prediction.getActualDateTime());
163 predictions.add(prediction);
164 } catch (BuienradarParseException e) {
165 String error = e.getMessage();
166 errors.add(error != null ? error : "null");
169 if (!errors.isEmpty()) {
170 logger.warn("Could not parse all results: {}", errors.stream().collect(Collectors.joining(", ")));
172 return Optional.of(predictions);