2 * Copyright (c) 2010-2023 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.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;
62 import com.google.gson.Gson;
63 import com.google.gson.GsonBuilder;
66 * The {@link WeatherCompanyAbstractHandler} contains common utilities used by
69 * Weather Company API documentation is located here
70 * - https://docs.google.com/document/d/1eKCnKXI9xnoMGRRzOL1xPCBihNV2rOet08qpE_gArAY/edit
72 * @author Mark Hilbush - Initial contribution
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;
79 private final Logger logger = LoggerFactory.getLogger(WeatherCompanyAbstractHandler.class);
81 protected final Gson gson = new GsonBuilder().serializeNulls().create();
83 protected final Map<String, State> weatherDataCache = Collections.synchronizedMap(new HashMap<>());
85 // Provided by handler factory
86 private final TimeZoneProvider timeZoneProvider;
87 private final HttpClient httpClient;
88 private final SystemOfUnits systemOfUnits;
90 public WeatherCompanyAbstractHandler(Thing thing, TimeZoneProvider timeZoneProvider, HttpClient httpClient,
91 UnitProvider unitProvider) {
93 this.timeZoneProvider = timeZoneProvider;
94 this.httpClient = httpClient;
95 this.systemOfUnits = unitProvider.getMeasurementSystem();
99 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
100 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
101 updateStatus(ThingStatus.ONLINE);
103 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
107 protected boolean isBridgeOnline() {
108 boolean bridgeStatus = false;
109 Bridge bridge = getBridge();
110 if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
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();
132 * Set either Imperial or Metric SI for the API call
134 protected String getUnitsQueryString() {
135 return isImperial() ? "e" : "s";
139 * Determine the units configured in the system
141 protected boolean isImperial() {
142 return systemOfUnits instanceof ImperialUnits ? true : false;
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);
154 * Set the state to the passed value. If value is null, set the state to UNDEF
156 protected State undefOrString(@Nullable String value) {
157 return value == null ? UnDefType.UNDEF : new StringType(value);
160 protected State undefOrDate(@Nullable Integer value) {
161 return value == null ? UnDefType.UNDEF : getLocalDateTimeType(value);
164 protected State undefOrDate(@Nullable String value) {
165 return value == null ? UnDefType.UNDEF : getLocalDateTimeType(value);
168 protected State undefOrDecimal(@Nullable Number value) {
169 return value == null ? UnDefType.UNDEF : new DecimalType(value.doubleValue());
172 protected State undefOrQuantity(@Nullable Number value, Unit<?> unit) {
173 return value == null ? UnDefType.UNDEF : new QuantityType<>(value, unit);
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()))
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.
186 protected Unit<?> getTempUnit() {
187 return isImperial() ? ImperialUnits.FAHRENHEIT : SIUnits.CELSIUS;
190 protected Unit<?> getSpeedUnit() {
191 return isImperial() ? ImperialUnits.MILES_PER_HOUR : Units.METRE_PER_SECOND;
194 protected Unit<?> getLengthUnit() {
195 return isImperial() ? ImperialUnits.INCH : MILLI(SIUnits.METRE);
199 * Execute the The Weather Channel API request
201 protected @Nullable String executeApiRequest(@Nullable String url) {
203 logger.debug("Handler: Can't execute request because url is null");
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");
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);
221 case HttpStatus.NO_CONTENT_204:
222 errorMsg = "HTTP response 400: No content. Check configuration";
224 case HttpStatus.BAD_REQUEST_400:
225 errorMsg = "HTTP response 400: Bad request";
227 case HttpStatus.UNAUTHORIZED_401:
228 errorMsg = "HTTP response 401: Unauthorized";
230 case HttpStatus.FORBIDDEN_403:
231 errorMsg = "HTTP response 403: Invalid API key";
233 case HttpStatus.NOT_FOUND_404:
234 errorMsg = "HTTP response 404: Endpoint not found";
236 case HttpStatus.METHOD_NOT_ALLOWED_405:
237 errorMsg = "HTTP response 405: Method not allowed";
239 case HttpStatus.NOT_ACCEPTABLE_406:
240 errorMsg = "HTTP response 406: Not acceptable";
242 case HttpStatus.REQUEST_TIMEOUT_408:
243 errorMsg = "HTTP response 408: Request timeout";
245 case HttpStatus.INTERNAL_SERVER_ERROR_500:
246 errorMsg = "HTTP response 500: Internal server error";
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());
255 errorMsg = String.format("HTTP GET failed: %d, %s", contentResponse.getStatus(),
256 contentResponse.getReason());
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());
266 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, 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 = {