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.darksky.internal.connection;
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.*;
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;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
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;
46 import com.google.gson.Gson;
47 import com.google.gson.JsonSyntaxException;
50 * The {@link DarkSkyConnection} is responsible for handling the connections to Dark Sky API.
52 * @author Christoph Weitkamp - Initial contribution
55 public class DarkSkyConnection {
57 private final Logger logger = LoggerFactory.getLogger(DarkSkyConnection.class);
59 private static final String PNG_CONTENT_TYPE = "image/png";
61 private static final String PARAM_EXCLUDE = "exclude";
62 private static final String PARAM_UNITS = "units";
63 private static final String PARAM_LANG = "lang";
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";
70 private final DarkSkyAPIHandler handler;
71 private final HttpClient httpClient;
73 private static final ByteArrayFileCache IMAGE_CACHE = new ByteArrayFileCache("org.openhab.binding.darksky");
74 private final ExpiringCacheMap<String, String> cache;
76 private final Gson gson = new Gson();
78 public DarkSkyConnection(DarkSkyAPIHandler handler, HttpClient httpClient) {
79 this.handler = handler;
80 this.httpClient = httpClient;
82 DarkSkyAPIConfiguration config = handler.getDarkSkyAPIConfig();
83 cache = new ExpiringCacheMap<>(TimeUnit.MINUTES.toMillis(config.refreshInterval));
87 * Requests the current weather data for the given location (see https://darksky.net/dev/docs#forecast-request).
89 * @param location location represented as {@link PointType}
90 * @return the current weather data
91 * @throws JsonSyntaxException
92 * @throws DarkSkyCommunicationException
93 * @throws DarkSkyConfigurationException
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");
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");
107 String url = String.format(Locale.ROOT, WEATHER_URL, apikey, location.getLatitude().doubleValue(),
108 location.getLongitude().doubleValue());
110 return gson.fromJson(getResponseFromCache(buildURL(url, getRequestParams(config))),
111 DarkSkyJsonWeatherData.class);
115 * Downloads the icon for the given icon id (see https://darksky.net/dev/docs/faq#icons).
117 * @param iconId the id of the icon
118 * @return the weather icon as {@link RawType}
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.");
125 return downloadWeatherIconFromCache(String.format(ICON_URL, iconId));
128 private static @Nullable RawType downloadWeatherIconFromCache(String url) {
129 if (IMAGE_CACHE.containsKey(url)) {
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 '{}'",
137 RawType image = downloadWeatherIcon(url);
139 IMAGE_CACHE.put(url, image.getBytes());
146 private static @Nullable RawType downloadWeatherIcon(String url) {
147 return HttpUtil.downloadImage(url);
150 private Map<String, String> getRequestParams(DarkSkyAPIConfiguration config) {
151 Map<String, String> params = new HashMap<>();
152 params.put(PARAM_EXCLUDE, "minutely,flags");
154 // params.put(PARAM_EXTEND, "hourly");
156 params.put(PARAM_UNITS, "si");
158 String language = config.language;
159 if (language != null && !(language = language.trim()).isEmpty()) {
160 params.put(PARAM_LANG, language.toLowerCase());
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 + "?", ""));
170 private String encodeParam(String value) {
172 return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
173 } catch (UnsupportedEncodingException e) {
174 logger.debug("UnsupportedEncodingException occurred during execution: {}", e.getLocalizedMessage(), e);
179 private @Nullable String getResponseFromCache(String url) {
180 return cache.putIfAbsentAndGet(url, () -> getResponse(url));
183 private String getResponse(String url) {
185 if (logger.isTraceEnabled()) {
186 logger.trace("Dark Sky request: URL = '{}'", uglifyApikey(url));
188 ContentResponse contentResponse = httpClient.newRequest(url).method(GET).timeout(10, TimeUnit.SECONDS)
190 int httpStatus = contentResponse.getStatus();
191 String content = contentResponse.getContentAsString();
192 logger.trace("Dark Sky response: status = {}, content = '{}'", httpStatus, content);
193 switch (httpStatus) {
196 case BAD_REQUEST_400:
197 case UNAUTHORIZED_401:
199 logger.debug("Dark Sky server responded with status code {}: {}", httpStatus, content);
200 throw new DarkSkyConfigurationException(content);
202 logger.debug("Dark Sky server responded with status code {}: {}", httpStatus, content);
203 throw new DarkSkyCommunicationException(content);
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());
212 throw new DarkSkyCommunicationException(errorMessage, e.getCause());
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());
224 private String uglifyApikey(String url) {
225 return url.replaceAll("(appid=)+\\w+", "appid=*****");