]> git.basschouten.com Git - openhab-addons.git/blob
58d0b80713c640d67822e3e32b61b6a548319ca5
[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.darksky.internal.connection;
14
15 import static java.util.stream.Collectors.joining;
16 import static org.eclipse.jetty.http.HttpMethod.GET;
17 import static org.eclipse.jetty.http.HttpStatus.*;
18
19 import java.io.IOException;
20 import java.io.UnsupportedEncodingException;
21 import java.net.URLEncoder;
22 import java.nio.charset.StandardCharsets;
23 import java.util.HashMap;
24 import java.util.Locale;
25 import java.util.Map;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.client.HttpResponseException;
34 import org.eclipse.jetty.client.api.ContentResponse;
35 import org.openhab.binding.darksky.internal.config.DarkSkyAPIConfiguration;
36 import org.openhab.binding.darksky.internal.handler.DarkSkyAPIHandler;
37 import org.openhab.binding.darksky.internal.model.DarkSkyJsonWeatherData;
38 import org.openhab.core.cache.ByteArrayFileCache;
39 import org.openhab.core.cache.ExpiringCacheMap;
40 import org.openhab.core.io.net.http.HttpUtil;
41 import org.openhab.core.library.types.PointType;
42 import org.openhab.core.library.types.RawType;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 import com.google.gson.Gson;
47 import com.google.gson.JsonSyntaxException;
48
49 /**
50  * The {@link DarkSkyConnection} is responsible for handling the connections to Dark Sky API.
51  *
52  * @author Christoph Weitkamp - Initial contribution
53  */
54 @NonNullByDefault
55 public class DarkSkyConnection {
56
57     private final Logger logger = LoggerFactory.getLogger(DarkSkyConnection.class);
58
59     private static final String PNG_CONTENT_TYPE = "image/png";
60
61     private static final String PARAM_EXCLUDE = "exclude";
62     private static final String PARAM_UNITS = "units";
63     private static final String PARAM_LANG = "lang";
64
65     // Current weather data (see https://darksky.net/dev/docs#forecast-request)
66     private static final String WEATHER_URL = "https://api.darksky.net/forecast/%s/%f,%f";
67     // Weather icons (see https://darksky.net/dev/docs/faq#icons)
68     private static final String ICON_URL = "https://darksky.net/images/weather-icons/%s.png";
69
70     private final DarkSkyAPIHandler handler;
71     private final HttpClient httpClient;
72
73     private static final ByteArrayFileCache IMAGE_CACHE = new ByteArrayFileCache("org.openhab.binding.darksky");
74     private final ExpiringCacheMap<String, String> cache;
75
76     private final Gson gson = new Gson();
77
78     public DarkSkyConnection(DarkSkyAPIHandler handler, HttpClient httpClient) {
79         this.handler = handler;
80         this.httpClient = httpClient;
81
82         DarkSkyAPIConfiguration config = handler.getDarkSkyAPIConfig();
83         cache = new ExpiringCacheMap<>(TimeUnit.MINUTES.toMillis(config.refreshInterval));
84     }
85
86     /**
87      * Requests the current weather data for the given location (see https://darksky.net/dev/docs#forecast-request).
88      *
89      * @param location location represented as {@link PointType}
90      * @return the current weather data
91      * @throws JsonSyntaxException
92      * @throws DarkSkyCommunicationException
93      * @throws DarkSkyConfigurationException
94      */
95     public synchronized @Nullable DarkSkyJsonWeatherData getWeatherData(@Nullable PointType location)
96             throws JsonSyntaxException, DarkSkyCommunicationException, DarkSkyConfigurationException {
97         if (location == null) {
98             throw new DarkSkyConfigurationException("@text/offline.conf-error-missing-location");
99         }
100
101         DarkSkyAPIConfiguration config = handler.getDarkSkyAPIConfig();
102         String apikey = config.apikey;
103         if (apikey == null || (apikey = apikey.trim()).isEmpty()) {
104             throw new DarkSkyConfigurationException("@text/offline.conf-error-missing-apikey");
105         }
106
107         String url = String.format(Locale.ROOT, WEATHER_URL, apikey, location.getLatitude().doubleValue(),
108                 location.getLongitude().doubleValue());
109
110         return gson.fromJson(getResponseFromCache(buildURL(url, getRequestParams(config))),
111                 DarkSkyJsonWeatherData.class);
112     }
113
114     /**
115      * Downloads the icon for the given icon id (see https://darksky.net/dev/docs/faq#icons).
116      *
117      * @param iconId the id of the icon
118      * @return the weather icon as {@link RawType}
119      */
120     public static @Nullable RawType getWeatherIcon(String iconId) {
121         if (iconId.isEmpty()) {
122             throw new IllegalArgumentException("Cannot download weather icon as icon id is null.");
123         }
124
125         return downloadWeatherIconFromCache(String.format(ICON_URL, iconId));
126     }
127
128     private static @Nullable RawType downloadWeatherIconFromCache(String url) {
129         if (IMAGE_CACHE.containsKey(url)) {
130             try {
131                 return new RawType(IMAGE_CACHE.get(url), PNG_CONTENT_TYPE);
132             } catch (IOException e) {
133                 LoggerFactory.getLogger(DarkSkyConnection.class).trace("Failed to download the content of URL '{}'",
134                         url, e);
135             }
136         } else {
137             RawType image = downloadWeatherIcon(url);
138             if (image != null) {
139                 IMAGE_CACHE.put(url, image.getBytes());
140                 return image;
141             }
142         }
143         return null;
144     }
145
146     private static @Nullable RawType downloadWeatherIcon(String url) {
147         return HttpUtil.downloadImage(url);
148     }
149
150     private Map<String, String> getRequestParams(DarkSkyAPIConfiguration config) {
151         Map<String, String> params = new HashMap<>();
152         params.put(PARAM_EXCLUDE, "minutely,flags");
153
154         // params.put(PARAM_EXTEND, "hourly");
155
156         params.put(PARAM_UNITS, "si");
157
158         String language = config.language;
159         if (language != null && !(language = language.trim()).isEmpty()) {
160             params.put(PARAM_LANG, language.toLowerCase());
161         }
162         return params;
163     }
164
165     private String buildURL(String url, Map<String, String> requestParams) {
166         return requestParams.entrySet().stream().map(e -> e.getKey() + "=" + encodeParam(e.getValue()))
167                 .collect(joining("&", url + "?", ""));
168     }
169
170     private String encodeParam(String value) {
171         try {
172             return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
173         } catch (UnsupportedEncodingException e) {
174             logger.debug("UnsupportedEncodingException occurred during execution: {}", e.getLocalizedMessage(), e);
175             return "";
176         }
177     }
178
179     private @Nullable String getResponseFromCache(String url) {
180         return cache.putIfAbsentAndGet(url, () -> getResponse(url));
181     }
182
183     private String getResponse(String url) {
184         try {
185             if (logger.isTraceEnabled()) {
186                 logger.trace("Dark Sky request: URL = '{}'", uglifyApikey(url));
187             }
188             ContentResponse contentResponse = httpClient.newRequest(url).method(GET).timeout(10, TimeUnit.SECONDS)
189                     .send();
190             int httpStatus = contentResponse.getStatus();
191             String content = contentResponse.getContentAsString();
192             logger.trace("Dark Sky response: status = {}, content = '{}'", httpStatus, content);
193             switch (httpStatus) {
194                 case OK_200:
195                     return content;
196                 case BAD_REQUEST_400:
197                 case UNAUTHORIZED_401:
198                 case NOT_FOUND_404:
199                     logger.debug("Dark Sky server responded with status code {}: {}", httpStatus, content);
200                     throw new DarkSkyConfigurationException(content);
201                 default:
202                     logger.debug("Dark Sky server responded with status code {}: {}", httpStatus, content);
203                     throw new DarkSkyCommunicationException(content);
204             }
205         } catch (ExecutionException e) {
206             String errorMessage = e.getLocalizedMessage();
207             logger.trace("Exception occurred during execution: {}", errorMessage, e);
208             if (e.getCause() instanceof HttpResponseException) {
209                 logger.debug("Dark Sky server responded with status code {}: Invalid API key.", UNAUTHORIZED_401);
210                 throw new DarkSkyConfigurationException("@text/offline.conf-error-invalid-apikey", e.getCause());
211             } else {
212                 throw new DarkSkyCommunicationException(errorMessage, e.getCause());
213             }
214         } catch (TimeoutException e) {
215             logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
216             throw new DarkSkyCommunicationException(e.getLocalizedMessage(), e.getCause());
217         } catch (InterruptedException e) {
218             logger.debug("Execution interrupted: {}", e.getLocalizedMessage(), e);
219             Thread.currentThread().interrupt();
220             throw new DarkSkyCommunicationException(e.getLocalizedMessage(), e.getCause());
221         }
222     }
223
224     private String uglifyApikey(String url) {
225         return url.replaceAll("(appid=)+\\w+", "appid=*****");
226     }
227 }