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