]> git.basschouten.com Git - openhab-addons.git/blob
e0281a77411dd934df9ebe51fa9a7b071cffcc10
[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.weathercompany.internal.handler;
14
15 import static org.openhab.binding.weathercompany.internal.WeatherCompanyBindingConstants.CONFIG_LANGUAGE_DEFAULT;
16 import static org.openhab.core.library.unit.MetricPrefix.MILLI;
17
18 import java.time.Instant;
19 import java.time.ZoneId;
20 import java.time.ZonedDateTime;
21 import java.time.format.DateTimeParseException;
22 import java.util.Collections;
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 javax.measure.Unit;
31 import javax.measure.spi.SystemOfUnits;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.eclipse.jetty.client.HttpClient;
36 import org.eclipse.jetty.client.api.ContentResponse;
37 import org.eclipse.jetty.client.api.Request;
38 import org.eclipse.jetty.http.HttpHeader;
39 import org.eclipse.jetty.http.HttpMethod;
40 import org.eclipse.jetty.http.HttpStatus;
41 import org.openhab.core.i18n.TimeZoneProvider;
42 import org.openhab.core.i18n.UnitProvider;
43 import org.openhab.core.library.types.DateTimeType;
44 import org.openhab.core.library.types.DecimalType;
45 import org.openhab.core.library.types.PointType;
46 import org.openhab.core.library.types.QuantityType;
47 import org.openhab.core.library.types.StringType;
48 import org.openhab.core.library.unit.ImperialUnits;
49 import org.openhab.core.library.unit.SIUnits;
50 import org.openhab.core.thing.Bridge;
51 import org.openhab.core.thing.Thing;
52 import org.openhab.core.thing.ThingStatus;
53 import org.openhab.core.thing.ThingStatusDetail;
54 import org.openhab.core.thing.ThingStatusInfo;
55 import org.openhab.core.thing.binding.BaseThingHandler;
56 import org.openhab.core.types.State;
57 import org.openhab.core.types.UnDefType;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60
61 import com.google.gson.Gson;
62 import com.google.gson.GsonBuilder;
63
64 /**
65  * The {@link WeatherCompanyAbstractHandler} contains common utilities used by
66  * handlers.
67  *
68  * Weather Company API documentation is located here
69  * - https://docs.google.com/document/d/1eKCnKXI9xnoMGRRzOL1xPCBihNV2rOet08qpE_gArAY/edit
70  *
71  * @author Mark Hilbush - Initial contribution
72  */
73 @NonNullByDefault
74 public abstract class WeatherCompanyAbstractHandler extends BaseThingHandler {
75     protected static final int WEATHER_COMPANY_API_TIMEOUT_SECONDS = 15;
76     protected static final int REFRESH_JOB_INITIAL_DELAY_SECONDS = 6;
77
78     private final Logger logger = LoggerFactory.getLogger(WeatherCompanyAbstractHandler.class);
79
80     protected final Gson gson = new GsonBuilder().serializeNulls().create();
81
82     protected final Map<String, State> weatherDataCache = Collections.synchronizedMap(new HashMap<>());
83
84     // Provided by handler factory
85     private final TimeZoneProvider timeZoneProvider;
86     private final HttpClient httpClient;
87     private final SystemOfUnits systemOfUnits;
88
89     public WeatherCompanyAbstractHandler(Thing thing, TimeZoneProvider timeZoneProvider, HttpClient httpClient,
90             UnitProvider unitProvider) {
91         super(thing);
92         this.timeZoneProvider = timeZoneProvider;
93         this.httpClient = httpClient;
94         this.systemOfUnits = unitProvider.getMeasurementSystem();
95     }
96
97     @Override
98     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
99         if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
100             updateStatus(ThingStatus.ONLINE);
101         } else {
102             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
103         }
104     }
105
106     protected boolean isBridgeOnline() {
107         boolean bridgeStatus = false;
108         Bridge bridge = getBridge();
109         if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
110             bridgeStatus = true;
111         }
112         return bridgeStatus;
113     }
114
115     protected String getApiKey() {
116         String apiKey = "unknown";
117         Bridge bridge = getBridge();
118         if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
119             WeatherCompanyBridgeHandler handler = (WeatherCompanyBridgeHandler) bridge.getHandler();
120             if (handler != null) {
121                 String key = handler.getApiKey();
122                 if (key != null) {
123                     apiKey = key;
124                 }
125             }
126         }
127         return apiKey;
128     }
129
130     /*
131      * Set either Imperial or Metric SI for the API call
132      */
133     protected String getUnitsQueryString() {
134         return isImperial() ? "e" : "s";
135     }
136
137     /*
138      * Determine the units configured in the system
139      */
140     protected boolean isImperial() {
141         return systemOfUnits instanceof ImperialUnits ? true : false;
142     }
143
144     protected void updateChannel(String channelId, State state) {
145         // Only update channel if it's linked
146         if (isLinked(channelId)) {
147             updateState(channelId, state);
148             weatherDataCache.put(channelId, state);
149         }
150     }
151
152     /*
153      * Set the state to the passed value. If value is null, set the state to UNDEF
154      */
155     protected State undefOrString(@Nullable String value) {
156         return value == null ? UnDefType.UNDEF : new StringType(value);
157     }
158
159     protected State undefOrDate(@Nullable Integer value) {
160         return value == null ? UnDefType.UNDEF : getLocalDateTimeType(value);
161     }
162
163     protected State undefOrDate(@Nullable String value) {
164         return value == null ? UnDefType.UNDEF : getLocalDateTimeType(value);
165     }
166
167     protected State undefOrDecimal(@Nullable Number value) {
168         return value == null ? UnDefType.UNDEF : new DecimalType(value.doubleValue());
169     }
170
171     protected State undefOrQuantity(@Nullable Number value, Unit<?> unit) {
172         return value == null ? UnDefType.UNDEF : new QuantityType<>(value, unit);
173     }
174
175     protected State undefOrPoint(@Nullable Number lat, @Nullable Number lon) {
176         return lat != null && lon != null
177                 ? new PointType(new DecimalType(lat.doubleValue()), new DecimalType(lon.doubleValue()))
178                 : UnDefType.UNDEF;
179     }
180
181     /*
182      * The API will request units based on openHAB's SystemOfUnits setting. Therefore,
183      * when setting the QuantityType state, make sure we use the proper unit.
184      */
185     protected Unit<?> getTempUnit() {
186         return isImperial() ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS;
187     }
188
189     protected Unit<?> getSpeedUnit() {
190         return isImperial() ? ImperialUnits.MILES_PER_HOUR : SIUnits.KILOMETRE_PER_HOUR;
191     }
192
193     protected Unit<?> getLengthUnit() {
194         return isImperial() ? ImperialUnits.INCH : MILLI(SIUnits.METRE);
195     }
196
197     /*
198      * Execute the The Weather Channel API request
199      */
200     protected @Nullable String executeApiRequest(@Nullable String url) {
201         if (url == null) {
202             logger.debug("Handler: Can't execute request because url is null");
203             return null;
204         }
205         Request request = httpClient.newRequest(url);
206         request.timeout(WEATHER_COMPANY_API_TIMEOUT_SECONDS, TimeUnit.SECONDS);
207         request.method(HttpMethod.GET);
208         request.header(HttpHeader.ACCEPT, "application/json");
209         request.header(HttpHeader.ACCEPT_ENCODING, "gzip");
210
211         String errorMsg;
212         try {
213             ContentResponse contentResponse = request.send();
214             switch (contentResponse.getStatus()) {
215                 case HttpStatus.OK_200:
216                     String response = contentResponse.getContentAsString();
217                     String cacheControl = contentResponse.getHeaders().get(HttpHeader.CACHE_CONTROL);
218                     logger.debug("Cache-Control header is {}", cacheControl);
219                     return response;
220                 case HttpStatus.NO_CONTENT_204:
221                     errorMsg = "HTTP response 400: No content. Check configuration";
222                     break;
223                 case HttpStatus.BAD_REQUEST_400:
224                     errorMsg = "HTTP response 400: Bad request";
225                     break;
226                 case HttpStatus.UNAUTHORIZED_401:
227                     errorMsg = "HTTP response 401: Unauthorized";
228                     break;
229                 case HttpStatus.FORBIDDEN_403:
230                     errorMsg = "HTTP response 403: Invalid API key";
231                     break;
232                 case HttpStatus.NOT_FOUND_404:
233                     errorMsg = "HTTP response 404: Endpoint not found";
234                     break;
235                 case HttpStatus.METHOD_NOT_ALLOWED_405:
236                     errorMsg = "HTTP response 405: Method not allowed";
237                     break;
238                 case HttpStatus.NOT_ACCEPTABLE_406:
239                     errorMsg = "HTTP response 406: Not acceptable";
240                     break;
241                 case HttpStatus.REQUEST_TIMEOUT_408:
242                     errorMsg = "HTTP response 408: Request timeout";
243                     break;
244                 case HttpStatus.INTERNAL_SERVER_ERROR_500:
245                     errorMsg = "HTTP response 500: Internal server error";
246                     break;
247                 case HttpStatus.BAD_GATEWAY_502:
248                 case HttpStatus.SERVICE_UNAVAILABLE_503:
249                 case HttpStatus.GATEWAY_TIMEOUT_504:
250                     errorMsg = String.format("HTTP response %d: Service unavailable or gateway issue",
251                             contentResponse.getStatus());
252                     break;
253                 default:
254                     errorMsg = String.format("HTTP GET failed: %d, %s", contentResponse.getStatus(),
255                             contentResponse.getReason());
256                     break;
257             }
258         } catch (TimeoutException e) {
259             errorMsg = "TimeoutException: Call to Weather Company API timed out";
260         } catch (ExecutionException e) {
261             errorMsg = String.format("ExecutionException: %s", e.getMessage());
262         } catch (InterruptedException e) {
263             errorMsg = String.format("InterruptedException: %s", e.getMessage());
264         }
265         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMsg);
266         logger.debug("{}", errorMsg);
267         return null;
268     }
269
270     /*
271      * Convert UTC Unix epoch seconds to local time
272      */
273     protected DateTimeType getLocalDateTimeType(long epochSeconds) {
274         Instant instant = Instant.ofEpochSecond(epochSeconds);
275         ZonedDateTime localDateTime = instant.atZone(getZoneId());
276         DateTimeType dateTimeType = new DateTimeType(localDateTime);
277         return dateTimeType;
278     }
279
280     /*
281      * Convert UTC time string to local time
282      * Input string is of form 2018-12-02T10:47:00.000Z
283      */
284     protected State getLocalDateTimeType(String dateTimeString) {
285         State dateTimeType;
286         try {
287             Instant instant = Instant.parse(dateTimeString);
288             ZonedDateTime localDateTime = instant.atZone(getZoneId());
289             dateTimeType = new DateTimeType(localDateTime);
290         } catch (DateTimeParseException e) {
291             logger.debug("Error parsing date/time string: {}", e.getMessage());
292             dateTimeType = UnDefType.UNDEF;
293         }
294         return dateTimeType;
295     }
296
297     private ZoneId getZoneId() {
298         return timeZoneProvider.getTimeZone();
299     }
300
301     /*
302      * Called by discovery service to get TWC language based on
303      * the locale configured in openHAB
304      */
305     public static String lookupLanguage(Locale locale) {
306         String ohLanguage = locale.getLanguage() + "-" + locale.getCountry();
307         for (String language : WEATHER_CHANNEL_LANGUAGES) {
308             if (language.equals(ohLanguage)) {
309                 return language;
310             }
311         }
312         return CONFIG_LANGUAGE_DEFAULT;
313     }
314
315   //@formatter:off
316     private static final String[] WEATHER_CHANNEL_LANGUAGES = {
317         "ar-AE",
318         "az-AZ",
319         "bg-BG",
320         "bn-BD",
321         "bn-IN",
322         "bs-BA",
323         "ca-ES",
324         "cs-CZ",
325         "da-DK",
326         "de-DE",
327         "el-GR",
328         "en-GB",
329         "en-IN",
330         "en-US",
331         "es-AR",
332         "es-ES",
333         "es-LA",
334         "es-MX",
335         "es-UN",
336         "es-US",
337         "et-EE",
338         "fa-IR",
339         "fi-FI",
340         "fr-CA",
341         "fr-FR",
342         "gu-IN",
343         "he-IL",
344         "hi-IN",
345         "hr-HR",
346         "hu-HU",
347         "in-ID",
348         "is-IS",
349         "it-IT",
350         "iw-IL",
351         "ja-JP",
352         "jv-ID",
353         "ka-GE",
354         "kk-KZ",
355         "kn-IN",
356         "ko-KR",
357         "lt-LT",
358         "lv-LV",
359         "mk-MK",
360         "mn-MN",
361         "ms-MY",
362         "nl-NL",
363         "no-NO",
364         "pl-PL",
365         "pt-BR",
366         "pt-PT",
367         "ro-RO",
368         "ru-RU",
369         "si-LK",
370         "sk-SK",
371         "sl-SI",
372         "sq-AL",
373         "sr-BA",
374         "sr-ME",
375         "sr-RS",
376         "sv-SE",
377         "sw-KE",
378         "ta-IN",
379         "ta-LK",
380         "te-IN",
381         "tg-TJ",
382         "th-TH",
383         "tk-TM",
384         "tl-PH",
385         "tr-TR",
386         "uk-UA",
387         "ur-PK",
388         "uz-UZ",
389         "vi-VN",
390         "zh-CN",
391         "zh-HK",
392         "zh-TW"
393     };
394   //@formatter:on
395 }