2 * Copyright (c) 2010-2020 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.weathercompany.internal.handler;
15 import static org.openhab.binding.weathercompany.internal.WeatherCompanyBindingConstants.CONFIG_LANGUAGE_DEFAULT;
16 import static org.openhab.core.library.unit.MetricPrefix.MILLI;
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;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
30 import javax.measure.Unit;
31 import javax.measure.spi.SystemOfUnits;
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;
61 import com.google.gson.Gson;
62 import com.google.gson.GsonBuilder;
65 * The {@link WeatherCompanyAbstractHandler} contains common utilities used by
68 * Weather Company API documentation is located here
69 * - https://docs.google.com/document/d/1eKCnKXI9xnoMGRRzOL1xPCBihNV2rOet08qpE_gArAY/edit
71 * @author Mark Hilbush - Initial contribution
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;
78 private final Logger logger = LoggerFactory.getLogger(WeatherCompanyAbstractHandler.class);
80 protected final Gson gson = new GsonBuilder().serializeNulls().create();
82 protected final Map<String, State> weatherDataCache = Collections.synchronizedMap(new HashMap<>());
84 // Provided by handler factory
85 private final TimeZoneProvider timeZoneProvider;
86 private final HttpClient httpClient;
87 private final SystemOfUnits systemOfUnits;
89 public WeatherCompanyAbstractHandler(Thing thing, TimeZoneProvider timeZoneProvider, HttpClient httpClient,
90 UnitProvider unitProvider) {
92 this.timeZoneProvider = timeZoneProvider;
93 this.httpClient = httpClient;
94 this.systemOfUnits = unitProvider.getMeasurementSystem();
98 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
99 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
100 updateStatus(ThingStatus.ONLINE);
102 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
106 protected boolean isBridgeOnline() {
107 boolean bridgeStatus = false;
108 Bridge bridge = getBridge();
109 if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
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();
131 * Set either Imperial or Metric SI for the API call
133 protected String getUnitsQueryString() {
134 return isImperial() ? "e" : "s";
138 * Determine the units configured in the system
140 protected boolean isImperial() {
141 return systemOfUnits instanceof ImperialUnits ? true : false;
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);
153 * Set the state to the passed value. If value is null, set the state to UNDEF
155 protected State undefOrString(@Nullable String value) {
156 return value == null ? UnDefType.UNDEF : new StringType(value);
159 protected State undefOrDate(@Nullable Integer value) {
160 return value == null ? UnDefType.UNDEF : getLocalDateTimeType(value);
163 protected State undefOrDate(@Nullable String value) {
164 return value == null ? UnDefType.UNDEF : getLocalDateTimeType(value);
167 protected State undefOrDecimal(@Nullable Number value) {
168 return value == null ? UnDefType.UNDEF : new DecimalType(value.doubleValue());
171 protected State undefOrQuantity(@Nullable Number value, Unit<?> unit) {
172 return value == null ? UnDefType.UNDEF : new QuantityType<>(value, unit);
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()))
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.
185 protected Unit<?> getTempUnit() {
186 return isImperial() ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS;
189 protected Unit<?> getSpeedUnit() {
190 return isImperial() ? ImperialUnits.MILES_PER_HOUR : SIUnits.KILOMETRE_PER_HOUR;
193 protected Unit<?> getLengthUnit() {
194 return isImperial() ? ImperialUnits.INCH : MILLI(SIUnits.METRE);
198 * Execute the The Weather Channel API request
200 protected @Nullable String executeApiRequest(@Nullable String url) {
202 logger.debug("Handler: Can't execute request because url is null");
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");
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);
220 case HttpStatus.NO_CONTENT_204:
221 errorMsg = "HTTP response 400: No content. Check configuration";
223 case HttpStatus.BAD_REQUEST_400:
224 errorMsg = "HTTP response 400: Bad request";
226 case HttpStatus.UNAUTHORIZED_401:
227 errorMsg = "HTTP response 401: Unauthorized";
229 case HttpStatus.FORBIDDEN_403:
230 errorMsg = "HTTP response 403: Invalid API key";
232 case HttpStatus.NOT_FOUND_404:
233 errorMsg = "HTTP response 404: Endpoint not found";
235 case HttpStatus.METHOD_NOT_ALLOWED_405:
236 errorMsg = "HTTP response 405: Method not allowed";
238 case HttpStatus.NOT_ACCEPTABLE_406:
239 errorMsg = "HTTP response 406: Not acceptable";
241 case HttpStatus.REQUEST_TIMEOUT_408:
242 errorMsg = "HTTP response 408: Request timeout";
244 case HttpStatus.INTERNAL_SERVER_ERROR_500:
245 errorMsg = "HTTP response 500: Internal server error";
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());
254 errorMsg = String.format("HTTP GET failed: %d, %s", contentResponse.getStatus(),
255 contentResponse.getReason());
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());
265 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMsg);
266 logger.debug("{}", errorMsg);
271 * Convert UTC Unix epoch seconds to local time
273 protected DateTimeType getLocalDateTimeType(long epochSeconds) {
274 Instant instant = Instant.ofEpochSecond(epochSeconds);
275 ZonedDateTime localDateTime = instant.atZone(getZoneId());
276 DateTimeType dateTimeType = new DateTimeType(localDateTime);
281 * Convert UTC time string to local time
282 * Input string is of form 2018-12-02T10:47:00.000Z
284 protected State getLocalDateTimeType(String dateTimeString) {
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;
297 private ZoneId getZoneId() {
298 return timeZoneProvider.getTimeZone();
302 * Called by discovery service to get TWC language based on
303 * the locale configured in openHAB
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)) {
312 return CONFIG_LANGUAGE_DEFAULT;
316 private static final String[] WEATHER_CHANNEL_LANGUAGES = {