]> git.basschouten.com Git - openhab-addons.git/blob
ea53c044f0eb00568a7e3d19922cb47404d9a1a7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.buienradarapi;
14
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;
28
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;
34
35 /**
36  * The {@link BuienradarPredictionAPI} class implements the methods for retrieving results from the buienradar.nl
37  * service.
38  *
39  * @author Edwin de Jong - Initial contribution
40  */
41 @NonNullByDefault
42 public class BuienradarPredictionAPI implements PredictionAPI {
43     private static final ZoneId ZONE_AMSTERDAM = ZoneId.of("Europe/Amsterdam");
44
45     private static final String BASE_ADDRESS = "https://gpsgadget.buienradar.nl/data/raintext";
46
47     private static final int TIMEOUT_MS = 3000;
48
49     private final Logger logger = LoggerFactory.getLogger(BuienradarPredictionAPI.class);
50
51     /**
52      * Parses a raw intensity string, such as <code>000</code>, into the right intensity in mm / hour.
53      *
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.
57      */
58     public static BigDecimal parseIntensity(String intensityStr) throws BuienradarParseException {
59         try {
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);
65         }
66     }
67
68     /**
69      * Parses a time string, such as <code>10:30</code> into a ZonedDateTime, using the reference ZonedDateTime to find
70      * the closest
71      * match.
72      *
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.
77      */
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");
82         }
83
84         final int hour = Integer.parseInt(timeElements[0]);
85         final int minute = Integer.parseInt(timeElements[1]);
86         final LocalTime time = LocalTime.of(hour, minute);
87
88         final LocalDate localDateInAmsterdam = now.withZoneSameInstant(ZONE_AMSTERDAM).toLocalDate();
89
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);
94         } else {
95             return tryDateTime;
96         }
97     }
98
99     /**
100      * Parses a line returned from the buienradar API service. An example line could be <code>100|23:00</code>.
101      *
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.
108      */
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));
115         }
116         final BigDecimal intensityOut = parseIntensity(lineElements[0]);
117         final ZonedDateTime dateTime = parseDateTime(lineElements[1], now);
118         return new Prediction() {
119
120             @Override
121             public final BigDecimal getIntensity() {
122                 return intensityOut;
123             }
124
125             @Override
126             public ZonedDateTime getDateTimeOfPrediction() {
127                 return dateTime;
128             }
129
130             @Override
131             public ZonedDateTime getActualDateTime() {
132                 return actual.orElseGet(this::getDateTimeOfPrediction);
133             }
134         };
135     }
136
137     @Override
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());
141
142         final String result;
143         try {
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();
148         }
149
150         if (result.trim().isEmpty()) {
151             logger.warn("Buienradar API at URI {} return empty result", address);
152             return Optional.empty();
153         }
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) {
160             try {
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");
167             }
168         }
169         if (!errors.isEmpty()) {
170             logger.warn("Could not parse all results: {}", errors.stream().collect(Collectors.joining(", ")));
171         }
172         return Optional.of(predictions);
173     }
174 }