]> git.basschouten.com Git - openhab-addons.git/blob
08158ab22f45cd213f1f15bb25b5c22b48d4dfdc
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.meteoblue.internal.handler;
14
15 import static org.openhab.core.library.unit.MetricPrefix.*;
16
17 import java.awt.image.BufferedImage;
18 import java.io.ByteArrayOutputStream;
19 import java.io.IOException;
20 import java.math.BigDecimal;
21 import java.math.RoundingMode;
22 import java.time.ZoneId;
23 import java.time.ZonedDateTime;
24 import java.util.Calendar;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
28 import javax.imageio.ImageIO;
29
30 import org.apache.commons.lang.StringUtils;
31 import org.openhab.binding.meteoblue.internal.Forecast;
32 import org.openhab.binding.meteoblue.internal.MeteoBlueConfiguration;
33 import org.openhab.binding.meteoblue.internal.json.JsonData;
34 import org.openhab.core.io.net.http.HttpUtil;
35 import org.openhab.core.library.items.ImageItem;
36 import org.openhab.core.library.types.DateTimeType;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.QuantityType;
39 import org.openhab.core.library.types.RawType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.library.unit.SIUnits;
42 import org.openhab.core.library.unit.SmartHomeUnits;
43 import org.openhab.core.thing.Bridge;
44 import org.openhab.core.thing.Channel;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.Thing;
47 import org.openhab.core.thing.ThingStatus;
48 import org.openhab.core.thing.ThingStatusDetail;
49 import org.openhab.core.thing.binding.BaseThingHandler;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.State;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 import com.google.gson.Gson;
56
57 /**
58  * The {@link MeteoBlueHandler} is responsible for handling commands
59  * sent to one of the channels.
60  *
61  * @author Chris Carman - Initial contribution
62  */
63 public class MeteoBlueHandler extends BaseThingHandler {
64     private final Logger logger = LoggerFactory.getLogger(MeteoBlueHandler.class);
65
66     private Bridge bridge;
67     private Forecast[] forecasts;
68     private Gson gson;
69     private JsonData weatherData;
70     private ScheduledFuture<?> refreshJob;
71     private boolean properlyConfigured;
72
73     public MeteoBlueHandler(Thing thing) {
74         super(thing);
75         gson = new Gson();
76         forecasts = new Forecast[7];
77     }
78
79     @Override
80     public void handleCommand(ChannelUID channelUID, Command command) {
81         if (properlyConfigured) {
82             logger.debug("Received command '{}' for channel '{}'", command, channelUID);
83             updateChannel(channelUID.getId());
84         }
85     }
86
87     @Override
88     public void initialize() {
89         logger.debug("Initializing the meteoblue handler...");
90
91         bridge = getBridge();
92         if (bridge == null) {
93             logger.warn("Unable to initialize meteoblue. No bridge was configured.");
94             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge not configured.");
95             return;
96         }
97
98         MeteoBlueConfiguration config = getConfigAs(MeteoBlueConfiguration.class);
99
100         if (StringUtils.isBlank(config.serviceType)) {
101             config.serviceType = MeteoBlueConfiguration.SERVICETYPE_NONCOMM;
102             logger.debug("Using default service type ({}).", config.serviceType);
103             return;
104         }
105
106         if (StringUtils.isBlank(config.location)) {
107             flagBadConfig("The location was not configured.");
108             return;
109         }
110
111         config.parseLocation();
112
113         if (config.latitude == null) {
114             flagBadConfig(String.format("Could not determine latitude from the defined location setting (%s).",
115                     config.location));
116             return;
117         }
118
119         if (config.latitude > 90.0 || config.latitude < -90.0) {
120             flagBadConfig(String.format("Specified latitude value (%d) is not valid.", config.latitude));
121             return;
122         }
123
124         if (config.longitude == null) {
125             flagBadConfig(String.format("Could not determine longitude from the defined location setting (%s).",
126                     config.location));
127             return;
128         }
129
130         if (config.longitude > 180.0 || config.longitude < -180.0) {
131             flagBadConfig(String.format("Specified longitude value (%d) is not valid.", config.longitude));
132             return;
133         }
134
135         updateStatus(ThingStatus.UNKNOWN);
136         startAutomaticRefresh(config);
137         properlyConfigured = true;
138     }
139
140     /**
141      * Marks the configuration as invalid.
142      */
143     private void flagBadConfig(String message) {
144         properlyConfigured = false;
145         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message);
146     }
147
148     /**
149      * Schedule a job to periodically refresh the weather data.
150      */
151     private void startAutomaticRefresh(MeteoBlueConfiguration config) {
152         if (refreshJob != null && !refreshJob.isCancelled()) {
153             logger.trace("Refresh job already exists.");
154             return;
155         }
156
157         Runnable runnable = () -> {
158             boolean updateSuccessful = false;
159
160             try {
161                 // Request new weather data
162                 updateSuccessful = updateWeatherData();
163
164                 if (updateSuccessful) {
165                     // build forecasts from the data
166                     for (int i = 0; i < 7; i++) {
167                         forecasts[i] = new Forecast(i, weatherData.getMetadata(), weatherData.getUnits(),
168                                 weatherData.getDataDay());
169                     }
170
171                     // Update all channels from the updated weather data
172                     for (Channel channel : getThing().getChannels()) {
173                         updateChannel(channel.getUID().getId());
174                     }
175                 }
176             } catch (Exception e) {
177                 logger.warn("Exception occurred during weather update: {}", e.getMessage(), e);
178             }
179         };
180
181         int period = config.refresh != null ? config.refresh : MeteoBlueConfiguration.DEFAULT_REFRESH;
182         refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, period, TimeUnit.MINUTES);
183     }
184
185     @Override
186     public void dispose() {
187         logger.debug("Disposing meteoblue handler.");
188
189         if (refreshJob != null && !refreshJob.isCancelled()) {
190             refreshJob.cancel(true);
191             refreshJob = null;
192         }
193     }
194
195     /**
196      * Update the channel from the last weather data retrieved.
197      *
198      * @param channelId the id of the channel to be updated
199      */
200     private void updateChannel(String channelId) {
201         Channel channel = getThing().getChannel(channelId);
202         if (channel == null || !isLinked(channelId)) {
203             logger.trace("Channel '{}' was null or not linked! Not updated.", channelId);
204             return;
205         }
206
207         // get the set of channel parameters.
208         // the first will be the forecast day (eg. forecastToday),
209         // and the second will be the datapoint (eg. snowFraction)
210         String[] channelParts = channelId.split("#");
211         String forecastDay = channelParts[0];
212         String datapointName = channelParts[1];
213         if (channelParts.length != 2) {
214             logger.debug("Skipped invalid channelId '{}'", channelId);
215             return;
216         }
217
218         logger.debug("Updating channel '{}'", channelId);
219         Forecast forecast = getForecast(forecastDay);
220         if (forecast == null) {
221             logger.debug("No forecast found for '{}'. Not updating.", forecastDay);
222             return;
223         }
224
225         Object datapoint = forecast.getDatapoint(datapointName);
226         logger.debug("Value for datapoint '{}' is '{}'", datapointName, datapoint);
227         if (datapoint == null) {
228             logger.debug("Couldn't get datapoint '{}' for '{}'. Not updating.", datapointName, forecastDay);
229             return;
230         }
231
232         // Build a State from this value
233         State state = null;
234         if (datapoint instanceof Calendar) {
235             state = new DateTimeType(
236                     ZonedDateTime.ofInstant(((Calendar) datapoint).toInstant(), ZoneId.systemDefault()));
237         } else if (datapoint instanceof Integer) {
238             state = getStateForType(channel.getAcceptedItemType(), (Integer) datapoint);
239         } else if (datapoint instanceof Number) {
240             BigDecimal decimalValue = new BigDecimal(datapoint.toString()).setScale(2, RoundingMode.HALF_UP);
241             state = getStateForType(channel.getAcceptedItemType(), decimalValue);
242         } else if (datapoint instanceof String) {
243             state = new StringType(datapoint.toString());
244         } else if (datapoint instanceof BufferedImage) {
245             ImageItem item = new ImageItem("rain area");
246             state = new RawType(renderImage((BufferedImage) datapoint), "image/png");
247             item.setState(state);
248         } else {
249             logger.debug("Unsupported value type {}", datapoint.getClass().getSimpleName());
250         }
251
252         // Update the channel
253         if (state != null) {
254             logger.trace("Updating channel with state value {}. (object type {})", state,
255                     datapoint.getClass().getSimpleName());
256             updateState(channelId, state);
257         }
258     }
259
260     private State getStateForType(String type, Integer value) {
261         return getStateForType(type, new BigDecimal(value));
262     }
263
264     private State getStateForType(String type, BigDecimal value) {
265         State state = new DecimalType(value);
266
267         if (type.equals("Number:Temperature")) {
268             state = new QuantityType<>(value, SIUnits.CELSIUS);
269         } else if (type.equals("Number:Length")) {
270             state = new QuantityType<>(value, MILLI(SIUnits.METRE));
271         } else if (type.equals("Number:Pressure")) {
272             state = new QuantityType<>(value, HECTO(SIUnits.PASCAL));
273         } else if (type.equals("Number:Speed")) {
274             state = new QuantityType<>(value, SmartHomeUnits.METRE_PER_SECOND);
275         }
276
277         return state;
278     }
279
280     // Request new weather data from the service
281     private boolean updateWeatherData() {
282         if (bridge == null) {
283             logger.debug("Unable to update weather data. Bridge missing.");
284             return false;
285         }
286
287         MeteoBlueBridgeHandler handler = (MeteoBlueBridgeHandler) bridge.getHandler();
288         if (handler == null) {
289             logger.debug("Unable to update weather data. Handler missing.");
290             return false;
291         }
292
293         String apiKey = handler.getApiKey();
294
295         logger.debug("Updating weather data...");
296         MeteoBlueConfiguration config = getConfigAs(MeteoBlueConfiguration.class);
297         config.parseLocation();
298         String serviceType = config.serviceType;
299
300         if (serviceType.equals(MeteoBlueConfiguration.SERVICETYPE_COMM)) {
301             logger.debug("Fetching weather data using Commercial API.");
302         } else {
303             logger.debug("Fetching weather data using NonCommercial API.");
304         }
305
306         // get the base url for the HTTP query
307         String url = MeteoBlueConfiguration.getURL(serviceType);
308         url = url.replace("#API_KEY#", apiKey);
309         url = url.replace("#LATITUDE#", String.valueOf(config.latitude)).replace("#LONGITUDE#",
310                 String.valueOf(config.longitude));
311
312         // fill in any optional parameters for the HTTP query
313         StringBuilder builder = new StringBuilder();
314
315         if (config.altitude != null) {
316             builder.append("&asl=" + config.altitude);
317         }
318         if (StringUtils.isNotBlank(config.timeZone)) {
319             builder.append("&tz=" + config.timeZone);
320         }
321         url = url.replace("#FORMAT_PARAMS#", builder.toString());
322         logger.trace("Using URL '{}'", url);
323
324         // Run the HTTP request and get the JSON response
325         String httpResponse = getWeatherData(url);
326         if (httpResponse == null) {
327             return false;
328         }
329         JsonData jsonResult = translateJson(httpResponse, serviceType);
330         logger.trace("json object: {}", jsonResult);
331
332         if (jsonResult == null) {
333             logger.warn("No data was received from the weather service");
334             return false;
335         }
336
337         String errorMessage = jsonResult.getErrorMessage();
338         if (errorMessage != null) {
339             if (errorMessage.equals("MB_REQUEST::DISPATCH: Invalid api key")) {
340                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid API Key");
341             } else if (errorMessage.equals("MB_REQUEST::DISPATCH: This datafeed is not authorized for your api key")) {
342                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
343                         "API Key not authorized for this datafeed");
344             } else {
345                 logger.warn("Failed to retrieve weather data due to unexpected error. Error message: {}", errorMessage);
346                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage);
347             }
348             return false;
349         }
350
351         weatherData = jsonResult;
352         updateStatus(ThingStatus.ONLINE);
353         return true;
354     }
355
356     // Run the HTTP request and get the JSON response
357     private String getWeatherData(String url) {
358         try {
359             String httpResponse = HttpUtil.executeUrl("GET", url, 30 * 1000);
360             logger.trace("http response: {}", httpResponse);
361             return httpResponse;
362         } catch (IOException e) {
363             logger.debug("I/O Exception occurred while retrieving weather data.", e);
364             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
365                     "I/O Exception occurred while retrieving weather data.");
366             return null;
367         }
368     }
369
370     // Convert a json string response into a json data object
371     private JsonData translateJson(String stringData, String serviceType) {
372         // JsonData weatherData = null;
373
374         // For now, no distinction is made between commercial and non-commercial data;
375         // This may need to be changed later based on user feedback.
376         /*
377          * if (serviceType.equals(MeteoBlueConfiguration.SERVICETYPE_COMM)) {
378          * weatherData = gson.fromJson(httpResponse, JsonCommercialData.class);
379          * }
380          * else {
381          * weatherData = gson.fromJson(httpResponse, JsonNonCommercialData.class);
382          * }
383          */
384
385         return gson.fromJson(stringData, JsonData.class);
386     }
387
388     private Forecast getForecast(String which) {
389         switch (which) {
390             case "forecastToday":
391                 return forecasts[0];
392             case "forecastTomorrow":
393                 return forecasts[1];
394             case "forecastDay2":
395                 return forecasts[2];
396             case "forecastDay3":
397                 return forecasts[3];
398             case "forecastDay4":
399                 return forecasts[4];
400             case "forecastDay5":
401                 return forecasts[5];
402             case "forecastDay6":
403                 return forecasts[6];
404             default:
405                 return null;
406         }
407     }
408
409     private byte[] renderImage(BufferedImage image) {
410         byte[] data = null;
411
412         try {
413             ByteArrayOutputStream out = new ByteArrayOutputStream();
414             ImageIO.write(image, "png", out);
415             out.flush();
416             data = out.toByteArray();
417             out.close();
418         } catch (IOException ioe) {
419             logger.debug("I/O exception occurred converting image data", ioe);
420         }
421
422         return data;
423     }
424 }