]> git.basschouten.com Git - openhab-addons.git/blob
0ab5fe4eb0ac0a89690d5088652dc0c0490b7223
[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.weatherunderground.internal.handler;
14
15 import static org.openhab.core.library.unit.MetricPrefix.*;
16
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;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.stream.Collectors;
27 import java.util.stream.Stream;
28
29 import javax.measure.Quantity;
30 import javax.measure.Unit;
31
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;
65
66 import com.google.gson.Gson;
67 import com.google.gson.JsonSyntaxException;
68
69 /**
70  * The {@link WeatherUndergroundHandler} is responsible for handling the
71  * weather things created to use the Weather Underground Service.
72  *
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
76  */
77 @NonNullByDefault
78 public class WeatherUndergroundHandler extends BaseThingHandler {
79
80     private final Logger logger = LoggerFactory.getLogger(WeatherUndergroundHandler.class);
81
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());
89
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
92     static {
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");
138         // Low German => ND
139         LANG_ISO_TO_WU_CODES.put("MK", "MK");
140         LANG_ISO_TO_WU_CODES.put("MT", "MT");
141         // Mandinka => GM
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");
160         // Swiss => CH
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");
173     }
174     private static final Map<String, String> LANG_COUNTRY_TO_WU_CODES = new HashMap<>();
175     static {
176         LANG_COUNTRY_TO_WU_CODES.put("en-GB", "LI"); // British English
177         LANG_COUNTRY_TO_WU_CODES.put("fr-CA", "FC"); // French Canadian
178     }
179
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;
185
186     private @Nullable ScheduledFuture<?> refreshJob;
187
188     private @Nullable WeatherUndergroundJsonData weatherData;
189
190     private @Nullable WeatherUndergroundBridgeHandler bridgeHandler;
191
192     public WeatherUndergroundHandler(Thing thing, LocaleProvider localeProvider, UnitProvider unitProvider,
193             TimeZoneProvider timeZoneProvider) {
194         super(thing);
195         this.localeProvider = localeProvider;
196         this.unitProvider = unitProvider;
197         this.timeZoneProvider = timeZoneProvider;
198         gson = new Gson();
199         forecastMap = initForecastDayMap();
200     }
201
202     @Override
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);
208         } else {
209             initializeThingHandler(bridge.getHandler(), bridge.getStatus());
210         }
211     }
212
213     @Override
214     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
215         logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
216         Bridge bridge = getBridge();
217         if (bridge == null) {
218             initializeThingHandler(null, bridgeStatusInfo.getStatus());
219         } else {
220             initializeThingHandler(bridge.getHandler(), bridgeStatusInfo.getStatus());
221         }
222     }
223
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;
229
230                 WeatherUndergroundConfiguration config = getConfigAs(WeatherUndergroundConfiguration.class);
231
232                 logger.debug("config location = {}", config.location);
233                 logger.debug("config language = {}", config.language);
234                 logger.debug("config refresh = {}", config.refresh);
235
236                 boolean validConfig = true;
237                 String errors = "";
238                 String statusDescr = null;
239
240                 if (config.location == null || config.location.trim().isEmpty()) {
241                     errors += " Parameter 'location' must be configured.";
242                     statusDescr = "@text/offline.conf-error-missing-location";
243                     validConfig = false;
244                 }
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";
249                         validConfig = false;
250                     }
251                 }
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";
255                     validConfig = false;
256                 }
257                 errors = errors.trim();
258
259                 if (validConfig) {
260                     updateStatus(ThingStatus.ONLINE);
261                     startAutomaticRefresh();
262                 } else {
263                     logger.debug("Setting thing '{}' to OFFLINE: {}", getThing().getUID(), errors);
264                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, statusDescr);
265                 }
266             } else {
267                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
268             }
269         } else {
270             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
271         }
272     }
273
274     /**
275      * Start the job refreshing the weather data
276      */
277     private void startAutomaticRefresh() {
278         ScheduledFuture<?> job = refreshJob;
279         if (job == null || job.isCancelled()) {
280             Runnable runnable = new Runnable() {
281                 @Override
282                 public void run() {
283                     try {
284                         // Request new weather data to the Weather Underground service
285                         updateWeatherData(USUAL_FEATURES);
286
287                         // Update all channels from the updated weather data
288                         for (Channel channel : getThing().getChannels()) {
289                             updateChannel(channel.getUID().getId());
290                         }
291                     } catch (Exception e) {
292                         logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
293                     }
294                 }
295             };
296
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);
300         }
301     }
302
303     @Override
304     public void dispose() {
305         logger.debug("Disposing WeatherUnderground handler.");
306
307         ScheduledFuture<?> job = refreshJob;
308         if (job != null) {
309             job.cancel(true);
310         }
311         refreshJob = null;
312     }
313
314     @Override
315     public void handleCommand(ChannelUID channelUID, Command command) {
316         if (command instanceof RefreshType) {
317             updateChannel(channelUID.getId());
318         } else {
319             logger.debug("The Weather Underground binding is a read-only binding and cannot handle command {}",
320                     command);
321         }
322     }
323
324     /**
325      * Update the channel from the last Weather Underground data retrieved
326      *
327      * @param channelId the id identifying the channel to be updated
328      */
329     private void updateChannel(String channelId) {
330         if (isLinked(channelId)) {
331             State state = null;
332             WeatherUndergroundJsonData data = weatherData;
333             if (data != null) {
334                 if (channelId.startsWith("current")) {
335                     state = updateCurrentObservationChannel(channelId, data.getCurrent());
336                 } else if (channelId.startsWith("forecast")) {
337                     state = updateForecastChannel(channelId, data.getForecast());
338                 }
339             }
340
341             logger.debug("Update channel {} with state {}", channelId, (state == null) ? "null" : state.toString());
342
343             // Update the channel
344             if (state != null) {
345                 updateState(channelId, state);
346             } else {
347                 updateState(channelId, UnDefType.NULL);
348             }
349         }
350     }
351
352     private @Nullable State updateCurrentObservationChannel(String channelId, WeatherUndergroundJsonCurrent current) {
353         WUQuantity quantity;
354         String channelTypeId = getChannelTypeId(channelId);
355         switch (channelTypeId) {
356             case "location":
357                 return undefOrState(current.getLocation(), new StringType(current.getLocation()));
358             case "stationId":
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)));
364             case "conditions":
365                 return undefOrState(current.getConditions(), new StringType(current.getConditions()));
366             case "temperature":
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));
377             case "windSpeed":
378                 quantity = getSpeed(current.getWindSpeedKmh(), current.getWindSpeedMph());
379                 return undefOrQuantity(quantity);
380             case "windGust":
381                 quantity = getSpeed(current.getWindGustKmh(), current.getWindGustMph());
382                 return undefOrQuantity(quantity);
383             case "pressure":
384                 quantity = getPressure(current.getPressureHPa(), current.getPressureInHg());
385                 return undefOrQuantity(quantity);
386             case "pressureTrend":
387                 return undefOrState(current.getPressureTrend(), new StringType(current.getPressureTrend()));
388             case "dewPoint":
389                 quantity = getTemperature(current.getDewPointC(), current.getDewPointF());
390                 return undefOrQuantity(quantity);
391             case "heatIndex":
392                 quantity = getTemperature(current.getHeatIndexC(), current.getHeatIndexF());
393                 return undefOrQuantity(quantity);
394             case "windChill":
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);
400             case "visibility":
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));
406             case "UVIndex":
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);
414             case "iconKey":
415                 return undefOrState(current.getIconKey(), new StringType(current.getIconKey()));
416             case "icon":
417                 State icon = HttpUtil.downloadImage(current.getIcon().toExternalForm());
418                 if (icon == null) {
419                     logger.debug("Failed to download the content of URL {}", current.getIcon().toExternalForm());
420                     return null;
421                 }
422                 return icon;
423             default:
424                 return null;
425         }
426     }
427
428     private @Nullable State updateForecastChannel(String channelId, WeatherUndergroundJsonForecast forecast) {
429         WUQuantity quantity;
430         int day = getDay(channelId);
431         WeatherUndergroundJsonForecastDay dayForecast = forecast.getSimpleForecast(day);
432
433         String channelTypeId = getChannelTypeId(channelId);
434         switch (channelTypeId) {
435             case "forecastTime":
436                 ZoneId zoneId = timeZoneProvider.getTimeZone();
437                 return undefOrState(dayForecast.getForecastTime(zoneId),
438                         new DateTimeType(dayForecast.getForecastTime(zoneId)));
439             case "conditions":
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);
456             case "snow":
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));
466             case "maxWindSpeed":
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);
478             case "iconKey":
479                 return undefOrState(dayForecast.getIconKey(), new StringType(dayForecast.getIconKey()));
480             case "icon":
481                 State icon = HttpUtil.downloadImage(dayForecast.getIcon().toExternalForm());
482                 if (icon == null) {
483                     logger.debug("Failed to download the content of URL {}", dayForecast.getIcon().toExternalForm());
484                     return null;
485                 }
486                 return icon;
487             default:
488                 return null;
489         }
490     }
491
492     private @Nullable State undefOrState(@Nullable Object value, State state) {
493         return value == null ? null : state;
494     }
495
496     private @Nullable <T extends Quantity<T>> State undefOrQuantity(WUQuantity quantity) {
497         return quantity.value == null ? null : new QuantityType<>(quantity.value, quantity.unit);
498     }
499
500     private @Nullable State undefOrDecimal(@Nullable Number value) {
501         return value == null ? null : new DecimalType(value.doubleValue());
502     }
503
504     private int getDay(String channelId) {
505         String channel = channelId.split("#")[0];
506
507         return forecastMap.get(channel);
508     }
509
510     private String getChannelTypeId(String channelId) {
511         return channelId.substring(channelId.indexOf("#") + 1);
512     }
513
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));
526         return forecastMap;
527     }
528
529     /**
530      * Request new current conditions and forecast 10 days to the Weather Underground service
531      * and store the data in weatherData
532      *
533      * @param features the list of features to be requested
534      * @return true if success or false in case of error
535      */
536     private boolean updateWeatherData(Set<String> features) {
537         WeatherUndergroundJsonData result = null;
538         boolean resultOk = false;
539         String error = null;
540         String errorDetail = null;
541         String statusDescr = null;
542
543         // Request new weather data to the Weather Underground service
544
545         try {
546             WeatherUndergroundConfiguration config = getConfigAs(WeatherUndergroundConfiguration.class);
547
548             String urlStr = URL_QUERY.replace("%FEATURES%", String.join("/", features));
549
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(),
555                         lang);
556             }
557             if (lang.isEmpty()) {
558                 urlStr = urlStr.replace("%SETTINGS%", "");
559             } else {
560                 urlStr = urlStr.replace("%SETTINGS%", "lang:" + lang.toUpperCase());
561             }
562
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%", "***"));
567             }
568
569             urlStr = urlStr.replace("%APIKEY%", bridgeHandler.getApikey());
570
571             // Run the HTTP request and get the JSON response from Weather Underground
572             String response = null;
573             try {
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";
581             }
582
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";
591                 }
592                 errorDetail = result.getResponse().getErrorDescription();
593             } else {
594                 resultOk = true;
595                 for (String feature : features) {
596                     if (feature.equals(FEATURE_CONDITIONS) && result.getCurrent() == null) {
597                         resultOk = false;
598                         errorDetail = "missing current_observation sub-object";
599                     } else if (feature.equals(FEATURE_FORECAST10DAY) && result.getForecast() == null) {
600                         resultOk = false;
601                         errorDetail = "missing forecast sub-object";
602                     } else if (feature.equals(FEATURE_GEOLOOKUP) && result.getLocation() == null) {
603                         resultOk = false;
604                         errorDetail = "missing location sub-object";
605                     }
606                 }
607             }
608             if (!resultOk && error == null) {
609                 error = "Error in Weather Underground response";
610                 statusDescr = "@text/offline.comm-error-response";
611             }
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";
620         }
621
622         // Update the thing status
623         if (resultOk) {
624             updateStatus(ThingStatus.ONLINE);
625             weatherData = result;
626         } else {
627             logger.debug("Setting thing '{}' to OFFLINE: Error '{}': {}", getThing().getUID(), error, errorDetail);
628             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, statusDescr);
629             weatherData = null;
630         }
631
632         return resultOk;
633     }
634
635     /**
636      * Get the WU code associated to a language
637      *
638      * @param locale the locale settings with language and country
639      * @return the associated WU code or an empty string if not found
640      */
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());
646         }
647         return language != null ? language : "";
648     }
649
650     private WUQuantity getTemperature(BigDecimal siValue, BigDecimal imperialValue) {
651         return getWUQuantity(SIUnits.CELSIUS, ImperialUnits.FAHRENHEIT, siValue, imperialValue);
652     }
653
654     private WUQuantity getSpeed(BigDecimal siValue, BigDecimal imperialValue) {
655         return getWUQuantity(SIUnits.KILOMETRE_PER_HOUR, ImperialUnits.MILES_PER_HOUR, siValue, imperialValue);
656     }
657
658     private WUQuantity getPressure(BigDecimal siValue, BigDecimal imperialValue) {
659         return getWUQuantity(HECTO(SIUnits.PASCAL), ImperialUnits.INCH_OF_MERCURY, siValue, imperialValue);
660     }
661
662     private WUQuantity getPrecipitation(BigDecimal siValue, BigDecimal imperialValue) {
663         return getWUQuantity(MILLI(SIUnits.METRE), ImperialUnits.INCH, siValue, imperialValue);
664     }
665
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);
670     }
671
672     private class WUQuantity {
673         private WUQuantity(BigDecimal value, Unit<?> unit) {
674             this.value = value;
675             this.unit = unit;
676         }
677
678         private final Unit<?> unit;
679         private final BigDecimal value;
680     }
681 }