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