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.weatherunderground.internal.handler;
15 import static org.openhab.core.library.unit.MetricPrefix.*;
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.time.ZoneId;
20 import java.util.HashMap;
21 import java.util.Locale;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.stream.Collectors;
27 import java.util.stream.Stream;
29 import javax.measure.Quantity;
30 import javax.measure.Unit;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.weatherunderground.internal.config.WeatherUndergroundConfiguration;
35 import org.openhab.binding.weatherunderground.internal.json.WeatherUndergroundJsonCurrent;
36 import org.openhab.binding.weatherunderground.internal.json.WeatherUndergroundJsonData;
37 import org.openhab.binding.weatherunderground.internal.json.WeatherUndergroundJsonForecast;
38 import org.openhab.binding.weatherunderground.internal.json.WeatherUndergroundJsonForecastDay;
39 import org.openhab.core.i18n.LocaleProvider;
40 import org.openhab.core.i18n.TimeZoneProvider;
41 import org.openhab.core.i18n.UnitProvider;
42 import org.openhab.core.io.net.http.HttpUtil;
43 import org.openhab.core.library.types.DateTimeType;
44 import org.openhab.core.library.types.DecimalType;
45 import org.openhab.core.library.types.QuantityType;
46 import org.openhab.core.library.types.StringType;
47 import org.openhab.core.library.unit.ImperialUnits;
48 import org.openhab.core.library.unit.SIUnits;
49 import org.openhab.core.library.unit.Units;
50 import org.openhab.core.thing.Bridge;
51 import org.openhab.core.thing.Channel;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatus;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.ThingStatusInfo;
57 import org.openhab.core.thing.binding.BaseThingHandler;
58 import org.openhab.core.thing.binding.ThingHandler;
59 import org.openhab.core.types.Command;
60 import org.openhab.core.types.RefreshType;
61 import org.openhab.core.types.State;
62 import org.openhab.core.types.UnDefType;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
66 import com.google.gson.Gson;
67 import com.google.gson.JsonSyntaxException;
70 * The {@link WeatherUndergroundHandler} is responsible for handling the
71 * weather things created to use the Weather Underground Service.
73 * @author Laurent Garnier - Initial contribution
74 * @author Theo Giovanna - Added a bridge for the API key
75 * @author Laurent Garnier - refactor bridge/thing handling
78 public class WeatherUndergroundHandler extends BaseThingHandler {
80 private final Logger logger = LoggerFactory.getLogger(WeatherUndergroundHandler.class);
82 private static final int DEFAULT_REFRESH_PERIOD = 30;
83 private static final String URL_QUERY = "http://api.wunderground.com/api/%APIKEY%/%FEATURES%/%SETTINGS%/q/%QUERY%.json";
84 private static final String FEATURE_CONDITIONS = "conditions";
85 private static final String FEATURE_FORECAST10DAY = "forecast10day";
86 private static final String FEATURE_GEOLOOKUP = "geolookup";
87 private static final Set<String> USUAL_FEATURES = Stream.of(FEATURE_CONDITIONS, FEATURE_FORECAST10DAY)
88 .collect(Collectors.toSet());
90 private static final Map<String, String> LANG_ISO_TO_WU_CODES = new HashMap<>();
91 // Codes from https://www.wunderground.com/weather/api/d/docs?d=language-support
93 LANG_ISO_TO_WU_CODES.put("AF", "AF");
94 LANG_ISO_TO_WU_CODES.put("SQ", "AL");
95 LANG_ISO_TO_WU_CODES.put("AR", "AR");
96 LANG_ISO_TO_WU_CODES.put("HY", "HY");
97 LANG_ISO_TO_WU_CODES.put("AZ", "AZ");
98 LANG_ISO_TO_WU_CODES.put("EU", "EU");
99 LANG_ISO_TO_WU_CODES.put("BE", "BY");
100 LANG_ISO_TO_WU_CODES.put("BG", "BU");
101 LANG_ISO_TO_WU_CODES.put("MY", "MY");
102 LANG_ISO_TO_WU_CODES.put("CA", "CA");
103 // Chinese - Simplified => CN
104 LANG_ISO_TO_WU_CODES.put("ZH", "TW");
105 LANG_ISO_TO_WU_CODES.put("HR", "CR");
106 LANG_ISO_TO_WU_CODES.put("CS", "CZ");
107 LANG_ISO_TO_WU_CODES.put("DA", "DK");
108 LANG_ISO_TO_WU_CODES.put("DV", "DV");
109 LANG_ISO_TO_WU_CODES.put("NL", "NL");
110 LANG_ISO_TO_WU_CODES.put("EN", "EN");
111 LANG_ISO_TO_WU_CODES.put("EO", "EO");
112 LANG_ISO_TO_WU_CODES.put("ET", "ET");
113 LANG_ISO_TO_WU_CODES.put("FA", "FA");
114 LANG_ISO_TO_WU_CODES.put("FI", "FI");
115 LANG_ISO_TO_WU_CODES.put("FR", "FR");
116 LANG_ISO_TO_WU_CODES.put("GL", "GZ");
117 LANG_ISO_TO_WU_CODES.put("DE", "DL");
118 LANG_ISO_TO_WU_CODES.put("KA", "KA");
119 LANG_ISO_TO_WU_CODES.put("EL", "GR");
120 LANG_ISO_TO_WU_CODES.put("GU", "GU");
121 LANG_ISO_TO_WU_CODES.put("HT", "HT");
122 LANG_ISO_TO_WU_CODES.put("HE", "IL");
123 LANG_ISO_TO_WU_CODES.put("HI", "HI");
124 LANG_ISO_TO_WU_CODES.put("HU", "HU");
125 LANG_ISO_TO_WU_CODES.put("IS", "IS");
126 LANG_ISO_TO_WU_CODES.put("IO", "IO");
127 LANG_ISO_TO_WU_CODES.put("ID", "ID");
128 LANG_ISO_TO_WU_CODES.put("GA", "IR");
129 LANG_ISO_TO_WU_CODES.put("IT", "IT");
130 LANG_ISO_TO_WU_CODES.put("JA", "JP");
131 LANG_ISO_TO_WU_CODES.put("JV", "JW");
132 LANG_ISO_TO_WU_CODES.put("KM", "KM");
133 LANG_ISO_TO_WU_CODES.put("KO", "KR");
134 LANG_ISO_TO_WU_CODES.put("KU", "KU");
135 LANG_ISO_TO_WU_CODES.put("LA", "LA");
136 LANG_ISO_TO_WU_CODES.put("LV", "LV");
137 LANG_ISO_TO_WU_CODES.put("LT", "LT");
139 LANG_ISO_TO_WU_CODES.put("MK", "MK");
140 LANG_ISO_TO_WU_CODES.put("MT", "MT");
142 LANG_ISO_TO_WU_CODES.put("MI", "MI");
143 LANG_ISO_TO_WU_CODES.put("MR", "MR");
144 LANG_ISO_TO_WU_CODES.put("MN", "MN");
145 LANG_ISO_TO_WU_CODES.put("NO", "NO");
146 LANG_ISO_TO_WU_CODES.put("OC", "OC");
147 LANG_ISO_TO_WU_CODES.put("PS", "PS");
148 // Plautdietsch => GN
149 LANG_ISO_TO_WU_CODES.put("PL", "PL");
150 LANG_ISO_TO_WU_CODES.put("PT", "BR");
151 LANG_ISO_TO_WU_CODES.put("PA", "PA");
152 LANG_ISO_TO_WU_CODES.put("RO", "RO");
153 LANG_ISO_TO_WU_CODES.put("RU", "RU");
154 LANG_ISO_TO_WU_CODES.put("SR", "SR");
155 LANG_ISO_TO_WU_CODES.put("SK", "SK");
156 LANG_ISO_TO_WU_CODES.put("SL", "SL");
157 LANG_ISO_TO_WU_CODES.put("ES", "SP");
158 LANG_ISO_TO_WU_CODES.put("SW", "SI");
159 LANG_ISO_TO_WU_CODES.put("SV", "SW");
161 LANG_ISO_TO_WU_CODES.put("TL", "TL");
162 LANG_ISO_TO_WU_CODES.put("TT", "TT");
163 LANG_ISO_TO_WU_CODES.put("TH", "TH");
164 LANG_ISO_TO_WU_CODES.put("TR", "TR");
165 LANG_ISO_TO_WU_CODES.put("TK", "TK");
166 LANG_ISO_TO_WU_CODES.put("UK", "UA");
167 LANG_ISO_TO_WU_CODES.put("UZ", "UZ");
168 LANG_ISO_TO_WU_CODES.put("VI", "VU");
169 LANG_ISO_TO_WU_CODES.put("CY", "CY");
170 LANG_ISO_TO_WU_CODES.put("WO", "SN");
171 // Yiddish - transliterated => JI
172 LANG_ISO_TO_WU_CODES.put("YI", "YI");
174 private static final Map<String, String> LANG_COUNTRY_TO_WU_CODES = new HashMap<>();
176 LANG_COUNTRY_TO_WU_CODES.put("en-GB", "LI"); // British English
177 LANG_COUNTRY_TO_WU_CODES.put("fr-CA", "FC"); // French Canadian
180 private final LocaleProvider localeProvider;
181 private final UnitProvider unitProvider;
182 private final TimeZoneProvider timeZoneProvider;
183 private final Gson gson;
184 private final Map<String, Integer> forecastMap;
186 private @Nullable ScheduledFuture<?> refreshJob;
188 private @Nullable WeatherUndergroundJsonData weatherData;
190 private @Nullable WeatherUndergroundBridgeHandler bridgeHandler;
192 public WeatherUndergroundHandler(Thing thing, LocaleProvider localeProvider, UnitProvider unitProvider,
193 TimeZoneProvider timeZoneProvider) {
195 this.localeProvider = localeProvider;
196 this.unitProvider = unitProvider;
197 this.timeZoneProvider = timeZoneProvider;
199 forecastMap = initForecastDayMap();
203 public void initialize() {
204 logger.debug("Initializing WeatherUnderground handler for thing {}", getThing().getUID());
205 Bridge bridge = getBridge();
206 if (bridge == null) {
207 initializeThingHandler(null, null);
209 initializeThingHandler(bridge.getHandler(), bridge.getStatus());
214 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
215 logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
216 Bridge bridge = getBridge();
217 if (bridge == null) {
218 initializeThingHandler(null, bridgeStatusInfo.getStatus());
220 initializeThingHandler(bridge.getHandler(), bridgeStatusInfo.getStatus());
224 private void initializeThingHandler(@Nullable ThingHandler bridgeHandler, @Nullable ThingStatus bridgeStatus) {
225 logger.debug("initializeThingHandler {}", getThing().getUID());
226 if (bridgeHandler != null && bridgeStatus != null) {
227 if (bridgeStatus == ThingStatus.ONLINE) {
228 this.bridgeHandler = (WeatherUndergroundBridgeHandler) bridgeHandler;
230 WeatherUndergroundConfiguration config = getConfigAs(WeatherUndergroundConfiguration.class);
232 logger.debug("config location = {}", config.location);
233 logger.debug("config language = {}", config.language);
234 logger.debug("config refresh = {}", config.refresh);
236 boolean validConfig = true;
238 String statusDescr = null;
240 if (config.location == null || config.location.trim().isEmpty()) {
241 errors += " Parameter 'location' must be configured.";
242 statusDescr = "@text/offline.conf-error-missing-location";
245 if (config.language != null) {
246 if (config.language.trim().length() != 2) {
247 errors += " Parameter 'language' must be 2 letters.";
248 statusDescr = "@text/offline.conf-error-syntax-language";
252 if (config.refresh != null && config.refresh < 5) {
253 errors += " Parameter 'refresh' must be at least 5 minutes.";
254 statusDescr = "@text/offline.conf-error-min-refresh";
257 errors = errors.trim();
260 updateStatus(ThingStatus.ONLINE);
261 startAutomaticRefresh();
263 logger.debug("Setting thing '{}' to OFFLINE: {}", getThing().getUID(), errors);
264 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, statusDescr);
267 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
270 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
275 * Start the job refreshing the weather data
277 private void startAutomaticRefresh() {
278 ScheduledFuture<?> job = refreshJob;
279 if (job == null || job.isCancelled()) {
280 Runnable runnable = new Runnable() {
284 // Request new weather data to the Weather Underground service
285 updateWeatherData(USUAL_FEATURES);
287 // Update all channels from the updated weather data
288 for (Channel channel : getThing().getChannels()) {
289 updateChannel(channel.getUID().getId());
291 } catch (Exception e) {
292 logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
297 WeatherUndergroundConfiguration config = getConfigAs(WeatherUndergroundConfiguration.class);
298 int period = (config.refresh != null) ? config.refresh.intValue() : DEFAULT_REFRESH_PERIOD;
299 refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, period, TimeUnit.MINUTES);
304 public void dispose() {
305 logger.debug("Disposing WeatherUnderground handler.");
307 ScheduledFuture<?> job = refreshJob;
315 public void handleCommand(ChannelUID channelUID, Command command) {
316 if (command instanceof RefreshType) {
317 updateChannel(channelUID.getId());
319 logger.debug("The Weather Underground binding is a read-only binding and cannot handle command {}",
325 * Update the channel from the last Weather Underground data retrieved
327 * @param channelId the id identifying the channel to be updated
329 private void updateChannel(String channelId) {
330 if (isLinked(channelId)) {
332 WeatherUndergroundJsonData data = weatherData;
334 if (channelId.startsWith("current")) {
335 state = updateCurrentObservationChannel(channelId, data.getCurrent());
336 } else if (channelId.startsWith("forecast")) {
337 state = updateForecastChannel(channelId, data.getForecast());
341 logger.debug("Update channel {} with state {}", channelId, (state == null) ? "null" : state.toString());
343 // Update the channel
345 updateState(channelId, state);
347 updateState(channelId, UnDefType.NULL);
352 private @Nullable State updateCurrentObservationChannel(String channelId, WeatherUndergroundJsonCurrent current) {
354 String channelTypeId = getChannelTypeId(channelId);
355 switch (channelTypeId) {
357 return undefOrState(current.getLocation(), new StringType(current.getLocation()));
359 return undefOrState(current.getStationId(), new StringType(current.getStationId()));
360 case "observationTime":
361 ZoneId zoneId = timeZoneProvider.getTimeZone();
362 return undefOrState(current.getObservationTime(zoneId),
363 new DateTimeType(current.getObservationTime(zoneId)));
365 return undefOrState(current.getConditions(), new StringType(current.getConditions()));
367 quantity = getTemperature(current.getTemperatureC(), current.getTemperatureF());
368 return undefOrQuantity(quantity);
369 case "relativeHumidity":
370 return undefOrState(current.getRelativeHumidity(),
371 new QuantityType<>(current.getRelativeHumidity(), Units.PERCENT));
372 case "windDirection":
373 return undefOrState(current.getWindDirection(), new StringType(current.getWindDirection()));
374 case "windDirectionDegrees":
375 return undefOrState(current.getWindDirectionDegrees(),
376 new QuantityType<>(current.getWindDirectionDegrees(), Units.DEGREE_ANGLE));
378 quantity = getSpeed(current.getWindSpeedKmh(), current.getWindSpeedMph());
379 return undefOrQuantity(quantity);
381 quantity = getSpeed(current.getWindGustKmh(), current.getWindGustMph());
382 return undefOrQuantity(quantity);
384 quantity = getPressure(current.getPressureHPa(), current.getPressureInHg());
385 return undefOrQuantity(quantity);
386 case "pressureTrend":
387 return undefOrState(current.getPressureTrend(), new StringType(current.getPressureTrend()));
389 quantity = getTemperature(current.getDewPointC(), current.getDewPointF());
390 return undefOrQuantity(quantity);
392 quantity = getTemperature(current.getHeatIndexC(), current.getHeatIndexF());
393 return undefOrQuantity(quantity);
395 quantity = getTemperature(current.getWindChillC(), current.getWindChillF());
396 return undefOrQuantity(quantity);
397 case "feelingTemperature":
398 quantity = getTemperature(current.getFeelingTemperatureC(), current.getFeelingTemperatureF());
399 return undefOrQuantity(quantity);
401 quantity = getWUQuantity(KILO(SIUnits.METRE), ImperialUnits.MILE, current.getVisibilityKm(),
402 current.getVisibilityMi());
403 return undefOrQuantity(quantity);
404 case "solarRadiation":
405 return undefOrQuantity(new WUQuantity(current.getSolarRadiation(), Units.IRRADIANCE));
407 return undefOrDecimal(current.getUVIndex());
408 case "precipitationDay":
409 quantity = getPrecipitation(current.getPrecipitationDayMm(), current.getPrecipitationDayIn());
410 return undefOrQuantity(quantity);
411 case "precipitationHour":
412 quantity = getPrecipitation(current.getPrecipitationHourMm(), current.getPrecipitationHourIn());
413 return undefOrQuantity(quantity);
415 return undefOrState(current.getIconKey(), new StringType(current.getIconKey()));
417 State icon = HttpUtil.downloadImage(current.getIcon().toExternalForm());
419 logger.debug("Failed to download the content of URL {}", current.getIcon().toExternalForm());
428 private @Nullable State updateForecastChannel(String channelId, WeatherUndergroundJsonForecast forecast) {
430 int day = getDay(channelId);
431 WeatherUndergroundJsonForecastDay dayForecast = forecast.getSimpleForecast(day);
433 String channelTypeId = getChannelTypeId(channelId);
434 switch (channelTypeId) {
436 ZoneId zoneId = timeZoneProvider.getTimeZone();
437 return undefOrState(dayForecast.getForecastTime(zoneId),
438 new DateTimeType(dayForecast.getForecastTime(zoneId)));
440 return undefOrState(dayForecast.getConditions(), new StringType(dayForecast.getConditions()));
441 case "minTemperature":
442 quantity = getTemperature(dayForecast.getMinTemperatureC(), dayForecast.getMinTemperatureF());
443 return undefOrQuantity(quantity);
444 case "maxTemperature":
445 quantity = getTemperature(dayForecast.getMaxTemperatureC(), dayForecast.getMaxTemperatureF());
446 return undefOrQuantity(quantity);
447 case "relativeHumidity":
448 return undefOrState(dayForecast.getRelativeHumidity(),
449 new QuantityType<>(dayForecast.getRelativeHumidity(), Units.PERCENT));
450 case "probaPrecipitation":
451 return undefOrState(dayForecast.getProbaPrecipitation(),
452 new QuantityType<>(dayForecast.getProbaPrecipitation(), Units.PERCENT));
453 case "precipitationDay":
454 quantity = getPrecipitation(dayForecast.getPrecipitationDayMm(), dayForecast.getPrecipitationDayIn());
455 return undefOrQuantity(quantity);
457 quantity = getWUQuantity(CENTI(SIUnits.METRE), ImperialUnits.INCH, dayForecast.getSnowCm(),
458 dayForecast.getSnowIn());
459 return undefOrQuantity(quantity);
460 case "maxWindDirection":
461 return undefOrState(dayForecast.getMaxWindDirection(),
462 new StringType(dayForecast.getMaxWindDirection()));
463 case "maxWindDirectionDegrees":
464 return undefOrState(dayForecast.getMaxWindDirectionDegrees(),
465 new QuantityType<>(dayForecast.getMaxWindDirectionDegrees(), Units.DEGREE_ANGLE));
467 quantity = getSpeed(dayForecast.getMaxWindSpeedKmh(), dayForecast.getMaxWindSpeedMph());
468 return undefOrQuantity(quantity);
469 case "averageWindDirection":
470 return undefOrState(dayForecast.getAverageWindDirection(),
471 new StringType(dayForecast.getAverageWindDirection()));
472 case "averageWindDirectionDegrees":
473 return undefOrState(dayForecast.getAverageWindDirectionDegrees(),
474 new QuantityType<>(dayForecast.getAverageWindDirectionDegrees(), Units.DEGREE_ANGLE));
475 case "averageWindSpeed":
476 quantity = getSpeed(dayForecast.getAverageWindSpeedKmh(), dayForecast.getAverageWindSpeedMph());
477 return undefOrQuantity(quantity);
479 return undefOrState(dayForecast.getIconKey(), new StringType(dayForecast.getIconKey()));
481 State icon = HttpUtil.downloadImage(dayForecast.getIcon().toExternalForm());
483 logger.debug("Failed to download the content of URL {}", dayForecast.getIcon().toExternalForm());
492 private @Nullable State undefOrState(@Nullable Object value, State state) {
493 return value == null ? null : state;
496 private @Nullable <T extends Quantity<T>> State undefOrQuantity(WUQuantity quantity) {
497 return quantity.value == null ? null : new QuantityType<>(quantity.value, quantity.unit);
500 private @Nullable State undefOrDecimal(@Nullable Number value) {
501 return value == null ? null : new DecimalType(value.doubleValue());
504 private int getDay(String channelId) {
505 String channel = channelId.split("#")[0];
507 return forecastMap.get(channel);
510 private String getChannelTypeId(String channelId) {
511 return channelId.substring(channelId.indexOf("#") + 1);
514 private Map<String, Integer> initForecastDayMap() {
515 Map<String, Integer> forecastMap = new HashMap<>();
516 forecastMap.put("forecastToday", Integer.valueOf(1));
517 forecastMap.put("forecastTomorrow", Integer.valueOf(2));
518 forecastMap.put("forecastDay2", Integer.valueOf(3));
519 forecastMap.put("forecastDay3", Integer.valueOf(4));
520 forecastMap.put("forecastDay4", Integer.valueOf(5));
521 forecastMap.put("forecastDay5", Integer.valueOf(6));
522 forecastMap.put("forecastDay6", Integer.valueOf(7));
523 forecastMap.put("forecastDay7", Integer.valueOf(8));
524 forecastMap.put("forecastDay8", Integer.valueOf(9));
525 forecastMap.put("forecastDay9", Integer.valueOf(10));
530 * Request new current conditions and forecast 10 days to the Weather Underground service
531 * and store the data in weatherData
533 * @param features the list of features to be requested
534 * @return true if success or false in case of error
536 private boolean updateWeatherData(Set<String> features) {
537 WeatherUndergroundJsonData result = null;
538 boolean resultOk = false;
540 String errorDetail = null;
541 String statusDescr = null;
543 // Request new weather data to the Weather Underground service
546 WeatherUndergroundConfiguration config = getConfigAs(WeatherUndergroundConfiguration.class);
548 String urlStr = URL_QUERY.replace("%FEATURES%", String.join("/", features));
550 String lang = config.language == null ? "" : config.language.trim();
551 if (lang.isEmpty()) {
552 // If language is not set in the configuration, you try deducing it from the system language
553 lang = getCodeFromLanguage(localeProvider.getLocale());
554 logger.debug("Use language deduced from system locale {}: {}", localeProvider.getLocale().getLanguage(),
557 if (lang.isEmpty()) {
558 urlStr = urlStr.replace("%SETTINGS%", "");
560 urlStr = urlStr.replace("%SETTINGS%", "lang:" + lang.toUpperCase());
563 String location = config.location == null ? "" : config.location.trim();
564 urlStr = urlStr.replace("%QUERY%", location);
565 if (logger.isDebugEnabled()) {
566 logger.debug("URL = {}", urlStr.replace("%APIKEY%", "***"));
569 urlStr = urlStr.replace("%APIKEY%", bridgeHandler.getApikey());
571 // Run the HTTP request and get the JSON response from Weather Underground
572 String response = null;
574 response = HttpUtil.executeUrl("GET", urlStr, WeatherUndergroundBridgeHandler.FETCH_TIMEOUT_MS);
575 logger.debug("weatherData = {}", response);
576 } catch (IllegalArgumentException e) {
577 // catch Illegal character in path at index XX: http://api.wunderground.com/...
578 error = "Error creating URI with location parameter: '" + location + "'";
579 errorDetail = e.getMessage();
580 statusDescr = "@text/offline.uri-error";
583 // Map the JSON response to an object
584 result = gson.fromJson(response, WeatherUndergroundJsonData.class);
585 if (result.getResponse() == null) {
586 errorDetail = "missing response sub-object";
587 } else if (result.getResponse().getErrorDescription() != null) {
588 if ("keynotfound".equals(result.getResponse().getErrorType())) {
589 error = "API key has to be fixed";
590 statusDescr = "@text/offline.comm-error-invalid-api-key";
592 errorDetail = result.getResponse().getErrorDescription();
595 for (String feature : features) {
596 if (feature.equals(FEATURE_CONDITIONS) && result.getCurrent() == null) {
598 errorDetail = "missing current_observation sub-object";
599 } else if (feature.equals(FEATURE_FORECAST10DAY) && result.getForecast() == null) {
601 errorDetail = "missing forecast sub-object";
602 } else if (feature.equals(FEATURE_GEOLOOKUP) && result.getLocation() == null) {
604 errorDetail = "missing location sub-object";
608 if (!resultOk && error == null) {
609 error = "Error in Weather Underground response";
610 statusDescr = "@text/offline.comm-error-response";
612 } catch (IOException e) {
613 error = "Error running Weather Underground request";
614 errorDetail = e.getMessage();
615 statusDescr = "@text/offline.comm-error-running-request";
616 } catch (JsonSyntaxException e) {
617 error = "Error parsing Weather Underground response";
618 errorDetail = e.getMessage();
619 statusDescr = "@text/offline.comm-error-parsing-response";
622 // Update the thing status
624 updateStatus(ThingStatus.ONLINE);
625 weatherData = result;
627 logger.debug("Setting thing '{}' to OFFLINE: Error '{}': {}", getThing().getUID(), error, errorDetail);
628 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, statusDescr);
636 * Get the WU code associated to a language
638 * @param locale the locale settings with language and country
639 * @return the associated WU code or an empty string if not found
641 public static String getCodeFromLanguage(Locale locale) {
642 String key = locale.getLanguage() + "-" + locale.getCountry();
643 String language = LANG_COUNTRY_TO_WU_CODES.get(key);
644 if (language == null) {
645 language = LANG_ISO_TO_WU_CODES.get(locale.getLanguage().toUpperCase());
647 return language != null ? language : "";
650 private WUQuantity getTemperature(BigDecimal siValue, BigDecimal imperialValue) {
651 return getWUQuantity(SIUnits.CELSIUS, ImperialUnits.FAHRENHEIT, siValue, imperialValue);
654 private WUQuantity getSpeed(BigDecimal siValue, BigDecimal imperialValue) {
655 return getWUQuantity(SIUnits.KILOMETRE_PER_HOUR, ImperialUnits.MILES_PER_HOUR, siValue, imperialValue);
658 private WUQuantity getPressure(BigDecimal siValue, BigDecimal imperialValue) {
659 return getWUQuantity(HECTO(SIUnits.PASCAL), ImperialUnits.INCH_OF_MERCURY, siValue, imperialValue);
662 private WUQuantity getPrecipitation(BigDecimal siValue, BigDecimal imperialValue) {
663 return getWUQuantity(MILLI(SIUnits.METRE), ImperialUnits.INCH, siValue, imperialValue);
666 private <T extends Quantity<T>> WUQuantity getWUQuantity(Unit<T> siUnit, Unit<T> imperialUnit, BigDecimal siValue,
667 BigDecimal imperialValue) {
668 boolean isSI = unitProvider.getMeasurementSystem().equals(SIUnits.getInstance());
669 return new WUQuantity(isSI ? siValue : imperialValue, isSI ? siUnit : imperialUnit);
672 private class WUQuantity {
673 private WUQuantity(BigDecimal value, Unit<?> unit) {
678 private final Unit<?> unit;
679 private final BigDecimal value;