]> git.basschouten.com Git - openhab-addons.git/blob
daf763e7ee1aa180ff70d69ca239dd95ac2d9208
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.openweathermap.internal.handler;
14
15 import static org.openhab.binding.openweathermap.internal.OpenWeatherMapBindingConstants.*;
16 import static org.openhab.core.library.unit.MetricPrefix.*;
17 import static org.openhab.core.library.unit.SIUnits.*;
18 import static org.openhab.core.library.unit.Units.*;
19 import static org.openhab.core.types.TimeSeries.Policy.REPLACE;
20
21 import java.time.Instant;
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.openweathermap.internal.config.OpenWeatherMapOneCallConfiguration;
30 import org.openhab.binding.openweathermap.internal.connection.OpenWeatherMapConnection;
31 import org.openhab.binding.openweathermap.internal.dto.OpenWeatherMapOneCallAPIData;
32 import org.openhab.binding.openweathermap.internal.dto.forecast.daily.FeelsLikeTemp;
33 import org.openhab.binding.openweathermap.internal.dto.forecast.daily.Temp;
34 import org.openhab.binding.openweathermap.internal.dto.onecall.Alert;
35 import org.openhab.binding.openweathermap.internal.dto.onecall.Daily;
36 import org.openhab.binding.openweathermap.internal.dto.onecall.Hourly;
37 import org.openhab.binding.openweathermap.internal.dto.onecall.Precipitation;
38 import org.openhab.core.i18n.CommunicationException;
39 import org.openhab.core.i18n.ConfigurationException;
40 import org.openhab.core.i18n.TimeZoneProvider;
41 import org.openhab.core.library.types.QuantityType;
42 import org.openhab.core.thing.Channel;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.binding.builder.ThingBuilder;
48 import org.openhab.core.types.State;
49 import org.openhab.core.types.TimeSeries;
50 import org.openhab.core.types.UnDefType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import com.google.gson.JsonSyntaxException;
55
56 /**
57  * The {@link OpenWeatherMapOneCallHandler} is responsible for handling commands, which are sent to one of
58  * the channels.
59  *
60  * @author Wolfgang Klimt - Initial contribution
61  * @author Christoph Weitkamp - Added weather alerts
62  * @author Florian Hotze - Added support for persisting forecasts
63  */
64 @NonNullByDefault
65 public class OpenWeatherMapOneCallHandler extends AbstractOpenWeatherMapHandler {
66
67     private final Logger logger = LoggerFactory.getLogger(OpenWeatherMapOneCallHandler.class);
68
69     private static final String CHANNEL_GROUP_MINUTELY_FORECAST_PREFIX = "forecastMinutes";
70     private static final String CHANNEL_GROUP_MINUTELY_TIMESERIES_PREFIX = "forecastMinutely";
71     private static final String CHANNEL_GROUP_HOURLY_FORECAST_PREFIX = "forecastHours";
72     private static final String CHANNEL_GROUP_HOURLY_TIMESERIES_PREFIX = "forecastHourly";
73     private static final String CHANNEL_GROUP_DAILY_FORECAST_PREFIX = "forecastDay";
74     private static final String CHANNEL_GROUP_DAILY_TIMESERIES_PREFIX = "forecastDaily";
75     private static final String CHANNEL_GROUP_ALERTS_PREFIX = "alerts";
76     private static final Pattern CHANNEL_GROUP_HOURLY_FORECAST_PREFIX_PATTERN = Pattern
77             .compile(CHANNEL_GROUP_HOURLY_FORECAST_PREFIX + "([0-9]*)");
78     private static final Pattern CHANNEL_GROUP_DAILY_FORECAST_PREFIX_PATTERN = Pattern
79             .compile(CHANNEL_GROUP_DAILY_FORECAST_PREFIX + "([0-9]*)");
80     private static final Pattern CHANNEL_GROUP_MINUTELY_FORECAST_PREFIX_PATTERN = Pattern
81             .compile(CHANNEL_GROUP_MINUTELY_FORECAST_PREFIX + "([0-9]*)");
82     private static final Pattern CHANNEL_GROUP_ALERTS_PREFIX_PATTERN = Pattern
83             .compile(CHANNEL_GROUP_ALERTS_PREFIX + "([0-9]*)");
84
85     private @Nullable OpenWeatherMapOneCallAPIData weatherData;
86
87     // forecastMinutes, -Hours and -Days determine the number of channel groups to create for each type
88     private int forecastMinutes = 60;
89     private int forecastHours = 48;
90     private int forecastDays = 8;
91     private int numberOfAlerts = 0;
92
93     public OpenWeatherMapOneCallHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
94         super(thing, timeZoneProvider);
95     }
96
97     @Override
98     public void initialize() {
99         super.initialize();
100         logger.debug("Initialize OpenWeatherMapOneCallHandler handler '{}'.", getThing().getUID());
101         OpenWeatherMapOneCallConfiguration config = getConfigAs(OpenWeatherMapOneCallConfiguration.class);
102
103         boolean configValid = true;
104         int newForecastMinutes = config.forecastMinutes;
105         if (newForecastMinutes < 0 || newForecastMinutes > 60) {
106             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
107                     "@text/offline.conf-error-not-supported-onecall-number-of-minutes");
108             configValid = false;
109         }
110         int newForecastHours = config.forecastHours;
111         if (newForecastHours < 0 || newForecastHours > 48) {
112             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
113                     "@text/offline.conf-error-not-supported-onecall-number-of-hours");
114             configValid = false;
115         }
116         int newForecastDays = config.forecastDays;
117         if (newForecastDays < 0 || newForecastDays > 8) {
118             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
119                     "@text/offline.conf-error-not-supported-onecall-number-of-days");
120             configValid = false;
121         }
122         int newNumberOfAlerts = config.numberOfAlerts;
123         if (newNumberOfAlerts < 0) {
124             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
125                     "@text/offline.conf-error-not-supported-onecall-number-of-alerts");
126             configValid = false;
127         }
128
129         if (configValid) {
130             logger.debug("Rebuilding thing '{}'.", getThing().getUID());
131             List<Channel> toBeAddedChannels = new ArrayList<>();
132             List<Channel> toBeRemovedChannels = new ArrayList<>();
133             toBeAddedChannels
134                     .addAll(createChannelsForGroup(CHANNEL_GROUP_ONECALL_CURRENT, CHANNEL_GROUP_TYPE_ONECALL_CURRENT));
135             if (forecastMinutes != newForecastMinutes) {
136                 logger.debug("forecastMinutes changed from {} to {}. Rebuilding minutely forecast channel groups.",
137                         forecastMinutes, newForecastMinutes);
138                 if (forecastMinutes > newForecastMinutes) {
139                     for (int i = newForecastMinutes + 1; i <= forecastMinutes; i++) {
140                         toBeRemovedChannels.addAll(removeChannelsOfGroup(
141                                 CHANNEL_GROUP_MINUTELY_FORECAST_PREFIX + ((i < 10) ? "0" : "") + Integer.toString(i)));
142                     }
143                 } else {
144                     for (int i = forecastMinutes + 1; i <= newForecastMinutes; i++) {
145                         toBeAddedChannels.addAll(createChannelsForGroup(
146                                 CHANNEL_GROUP_MINUTELY_FORECAST_PREFIX + ((i < 10) ? "0" : "") + Integer.toString(i),
147                                 CHANNEL_GROUP_TYPE_ONECALL_MINUTELY_FORECAST));
148                     }
149                 }
150                 forecastMinutes = newForecastMinutes;
151             }
152             if (forecastHours != newForecastHours) {
153                 logger.debug("ForecastHours changed from {} to {}. Rebuilding hourly forecast channel groups.",
154                         forecastHours, newForecastHours);
155                 if (forecastHours > newForecastHours) {
156                     for (int i = newForecastHours + 1; i <= forecastHours; i++) {
157                         toBeRemovedChannels.addAll(removeChannelsOfGroup(
158                                 CHANNEL_GROUP_HOURLY_FORECAST_PREFIX + ((i < 10) ? "0" : "") + Integer.toString(i)));
159                     }
160                 } else {
161                     for (int i = forecastHours + 1; i <= newForecastHours; i++) {
162                         toBeAddedChannels.addAll(createChannelsForGroup(
163                                 CHANNEL_GROUP_HOURLY_FORECAST_PREFIX + ((i < 10) ? "0" : "") + Integer.toString(i),
164                                 CHANNEL_GROUP_TYPE_ONECALL_HOURLY_FORECAST));
165                     }
166                 }
167                 forecastHours = newForecastHours;
168             }
169             if (forecastDays != newForecastDays) {
170                 logger.debug("ForecastDays changed from {} to {}. Rebuilding daily forecast channel groups.",
171                         forecastDays, newForecastDays);
172                 if (forecastDays > newForecastDays) {
173                     if (newForecastDays < 1) {
174                         toBeRemovedChannels.addAll(removeChannelsOfGroup(CHANNEL_GROUP_FORECAST_TODAY));
175                     }
176                     if (newForecastDays < 2) {
177                         toBeRemovedChannels.addAll(removeChannelsOfGroup(CHANNEL_GROUP_FORECAST_TOMORROW));
178                     }
179                     for (int i = newForecastDays; i < forecastDays; ++i) {
180                         toBeRemovedChannels.addAll(
181                                 removeChannelsOfGroup(CHANNEL_GROUP_DAILY_FORECAST_PREFIX + Integer.toString(i)));
182                     }
183                 } else {
184                     if (forecastDays == 0 && newForecastDays > 0) {
185                         toBeAddedChannels.addAll(createChannelsForGroup(CHANNEL_GROUP_FORECAST_TODAY,
186                                 CHANNEL_GROUP_TYPE_ONECALL_DAILY_FORECAST));
187                     }
188                     if (forecastDays <= 1 && newForecastDays > 1) {
189                         toBeAddedChannels.addAll(createChannelsForGroup(CHANNEL_GROUP_FORECAST_TOMORROW,
190                                 CHANNEL_GROUP_TYPE_ONECALL_DAILY_FORECAST));
191                     }
192                     for (int i = Math.max(forecastDays, 2); i < newForecastDays; ++i) {
193                         toBeAddedChannels.addAll(
194                                 createChannelsForGroup(CHANNEL_GROUP_DAILY_FORECAST_PREFIX + Integer.toString(i),
195                                         CHANNEL_GROUP_TYPE_ONECALL_DAILY_FORECAST));
196                     }
197                 }
198                 forecastDays = newForecastDays;
199                 if (numberOfAlerts != newNumberOfAlerts) {
200                     logger.debug("Rebuilding alerts channel groups.");
201                     if (numberOfAlerts > newNumberOfAlerts) {
202                         for (int i = newNumberOfAlerts + 1; i <= numberOfAlerts; ++i) {
203                             toBeRemovedChannels
204                                     .addAll(removeChannelsOfGroup(CHANNEL_GROUP_ALERTS_PREFIX + Integer.toString(i)));
205                         }
206                     } else {
207                         for (int i = numberOfAlerts + 1; i <= newNumberOfAlerts; ++i) {
208                             toBeAddedChannels
209                                     .addAll(createChannelsForGroup(CHANNEL_GROUP_ALERTS_PREFIX + Integer.toString(i),
210                                             CHANNEL_GROUP_TYPE_ONECALL_ALERTS));
211                         }
212                     }
213                     numberOfAlerts = newNumberOfAlerts;
214                 }
215             }
216             logger.debug("toBeRemovedChannels: {}. toBeAddedChannels: {}", toBeRemovedChannels, toBeAddedChannels);
217             ThingBuilder builder = editThing().withoutChannels(toBeRemovedChannels);
218             for (Channel channel : toBeAddedChannels) {
219                 builder.withChannel(channel);
220             }
221             updateThing(builder.build());
222         }
223     }
224
225     @Override
226     protected boolean requestData(OpenWeatherMapConnection connection)
227             throws CommunicationException, ConfigurationException {
228         logger.debug("Update weather and forecast data of thing '{}'.", getThing().getUID());
229         try {
230             // Include minutely, hourly and daily data as this is needed for the time series channels
231             weatherData = connection.getOneCallAPIData(location, false, false, false, numberOfAlerts == 0);
232             return true;
233         } catch (JsonSyntaxException e) {
234             logger.debug("JsonSyntaxException occurred during execution: {}", e.getMessage(), e);
235             return false;
236         }
237     }
238
239     @Override
240     protected void updateChannel(ChannelUID channelUID) {
241         String channelGroupId = channelUID.getGroupId();
242         logger.debug("OneCallHandler: updateChannel {}, groupID {}", channelUID, channelGroupId);
243         switch (channelGroupId) {
244             case CHANNEL_GROUP_ONECALL_CURRENT:
245                 updateCurrentChannel(channelUID);
246                 break;
247             case CHANNEL_GROUP_ONECALL_TODAY:
248                 updateDailyForecastChannel(channelUID, 0);
249                 break;
250             case CHANNEL_GROUP_ONECALL_TOMORROW:
251                 updateDailyForecastChannel(channelUID, 1);
252                 break;
253             case CHANNEL_GROUP_MINUTELY_TIMESERIES_PREFIX:
254                 updateMinutelyForecastTimeSeries(channelUID);
255                 break;
256             case CHANNEL_GROUP_HOURLY_TIMESERIES_PREFIX:
257                 updateHourlyForecastTimeSeries(channelUID);
258                 break;
259             case CHANNEL_GROUP_DAILY_TIMESERIES_PREFIX:
260                 updateDailyForecastTimeSeries(channelUID);
261                 break;
262             default:
263                 int i;
264                 Matcher hourlyForecastMatcher = CHANNEL_GROUP_HOURLY_FORECAST_PREFIX_PATTERN.matcher(channelGroupId);
265                 if (hourlyForecastMatcher.find() && (i = Integer.parseInt(hourlyForecastMatcher.group(1))) >= 1
266                         && i <= 48) {
267                     updateHourlyForecastChannel(channelUID, (i - 1));
268                     break;
269                 }
270                 Matcher dailyForecastMatcher = CHANNEL_GROUP_DAILY_FORECAST_PREFIX_PATTERN.matcher(channelGroupId);
271                 if (dailyForecastMatcher.find() && (i = Integer.parseInt(dailyForecastMatcher.group(1))) >= 1
272                         && i <= 7) {
273                     updateDailyForecastChannel(channelUID, i);
274                     break;
275                 }
276                 Matcher minutelyForecastMatcher = CHANNEL_GROUP_MINUTELY_FORECAST_PREFIX_PATTERN
277                         .matcher(channelGroupId);
278                 if (minutelyForecastMatcher.find() && (i = Integer.parseInt(minutelyForecastMatcher.group(1))) >= 1
279                         && i <= 60) {
280                     updateMinutelyForecastChannel(channelUID, i - 1);
281                     break;
282                 }
283                 Matcher alertsMatcher = CHANNEL_GROUP_ALERTS_PREFIX_PATTERN.matcher(channelGroupId);
284                 if (alertsMatcher.find() && (i = Integer.parseInt(alertsMatcher.group(1))) >= 1) {
285                     updateAlertsChannel(channelUID, i - 1);
286                     break;
287                 }
288                 break;
289         }
290     }
291
292     /**
293      * Update the channel from the last OpenWeatherMap data retrieved.
294      *
295      * @param channelUID the id identifying the channel to be updated
296      */
297     private void updateCurrentChannel(ChannelUID channelUID) {
298         String channelId = channelUID.getIdWithoutGroup();
299         String channelGroupId = channelUID.getGroupId();
300         OpenWeatherMapOneCallAPIData localWeatherData = weatherData;
301         if (localWeatherData != null) {
302             State state = UnDefType.UNDEF;
303             switch (channelId) {
304                 case CHANNEL_STATION_LOCATION:
305                     state = getPointTypeState(localWeatherData.getLat(), localWeatherData.getLon());
306                     break;
307                 case CHANNEL_TIME_STAMP:
308                     state = getDateTimeTypeState(localWeatherData.getCurrent().getDt());
309                     break;
310                 case CHANNEL_SUNRISE:
311                     state = getDateTimeTypeState(localWeatherData.getCurrent().getSunrise());
312                     break;
313                 case CHANNEL_SUNSET:
314                     state = getDateTimeTypeState(localWeatherData.getCurrent().getSunset());
315                     break;
316                 case CHANNEL_CONDITION:
317                     if (!localWeatherData.getCurrent().getWeather().isEmpty()) {
318                         state = getStringTypeState(localWeatherData.getCurrent().getWeather().get(0).getDescription());
319                     }
320                     break;
321                 case CHANNEL_CONDITION_ID:
322                     if (!localWeatherData.getCurrent().getWeather().isEmpty()) {
323                         state = getStringTypeState(
324                                 Integer.toString(localWeatherData.getCurrent().getWeather().get(0).getId()));
325                     }
326                     break;
327                 case CHANNEL_CONDITION_ICON:
328                     if (!localWeatherData.getCurrent().getWeather().isEmpty()) {
329                         state = getRawTypeState(OpenWeatherMapConnection
330                                 .getWeatherIcon(localWeatherData.getCurrent().getWeather().get(0).getIcon()));
331                     }
332                     break;
333                 case CHANNEL_CONDITION_ICON_ID:
334                     if (!localWeatherData.getCurrent().getWeather().isEmpty()) {
335                         state = getStringTypeState(localWeatherData.getCurrent().getWeather().get(0).getIcon());
336                     }
337                     break;
338                 case CHANNEL_TEMPERATURE:
339                     state = getQuantityTypeState(localWeatherData.getCurrent().getTemp(), CELSIUS);
340                     break;
341                 case CHANNEL_APPARENT_TEMPERATURE:
342                     state = getQuantityTypeState(localWeatherData.getCurrent().getFeelsLike(), CELSIUS);
343                     break;
344                 case CHANNEL_PRESSURE:
345                     state = getQuantityTypeState(localWeatherData.getCurrent().getPressure(), HECTO(PASCAL));
346                     break;
347                 case CHANNEL_HUMIDITY:
348                     state = getQuantityTypeState(localWeatherData.getCurrent().getHumidity(), PERCENT);
349                     break;
350                 case CHANNEL_DEW_POINT:
351                     state = getQuantityTypeState(localWeatherData.getCurrent().getDewPoint(), CELSIUS);
352                     break;
353                 case CHANNEL_WIND_SPEED:
354                     state = getQuantityTypeState(localWeatherData.getCurrent().getWindSpeed(), METRE_PER_SECOND);
355                     break;
356                 case CHANNEL_WIND_DIRECTION:
357                     state = getQuantityTypeState(localWeatherData.getCurrent().getWindDeg(), DEGREE_ANGLE);
358                     break;
359                 case CHANNEL_GUST_SPEED:
360                     state = getQuantityTypeState(localWeatherData.getCurrent().getWindGust(), METRE_PER_SECOND);
361                     break;
362                 case CHANNEL_CLOUDINESS:
363                     state = getQuantityTypeState(localWeatherData.getCurrent().getClouds(), PERCENT);
364                     break;
365                 case CHANNEL_UVINDEX:
366                     state = getDecimalTypeState(localWeatherData.getCurrent().getUvi());
367                     break;
368                 case CHANNEL_RAIN:
369                     Precipitation rain = localWeatherData.getCurrent().getRain();
370                     state = getQuantityTypeState(rain == null ? 0 : rain.get1h(), MILLI(METRE));
371                     break;
372                 case CHANNEL_SNOW:
373                     Precipitation snow = localWeatherData.getCurrent().getSnow();
374                     state = getQuantityTypeState(snow == null ? 0 : snow.get1h(), MILLI(METRE));
375                     break;
376                 case CHANNEL_VISIBILITY:
377                     State tempstate = new QuantityType<>(localWeatherData.getCurrent().getVisibility(), METRE)
378                             .toUnit(KILO(METRE));
379                     state = (tempstate == null ? state : tempstate);
380                     break;
381                 default:
382                     // This should not happen
383                     logger.warn("Unknown channel id {} in onecall current weather data", channelId);
384                     break;
385             }
386             logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
387             updateState(channelUID, state);
388         } else {
389             logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
390         }
391     }
392
393     /**
394      * Update the channel from the last OpenWeatherMap data retrieved.
395      *
396      * @param channelUID the id identifying the channel to be updated
397      * @param count the index of the minutely data referenced by the channel (minute 1 is count 0)
398      */
399     private void updateMinutelyForecastChannel(ChannelUID channelUID, int count) {
400         String channelId = channelUID.getIdWithoutGroup();
401         String channelGroupId = channelUID.getGroupId();
402         OpenWeatherMapOneCallAPIData localWeatherData = weatherData;
403         if (forecastMinutes == 0) {
404             logger.warn(
405                     "Can't update channel group {} because forecastMinutes is set to '0'. Please adjust config accordingly",
406                     channelGroupId);
407             return;
408         }
409         if (localWeatherData != null && localWeatherData.getMinutely() != null
410                 && localWeatherData.getMinutely().size() > count) {
411             org.openhab.binding.openweathermap.internal.dto.onecall.Minutely forecastData = localWeatherData
412                     .getMinutely().get(count);
413             State state = UnDefType.UNDEF;
414             switch (channelId) {
415                 case CHANNEL_TIME_STAMP:
416                     state = getDateTimeTypeState(forecastData.getDt());
417                     break;
418                 case CHANNEL_PRECIPITATION:
419                     double precipitation = forecastData.getPrecipitation();
420                     state = getQuantityTypeState(precipitation, MILLI(METRE));
421                     break;
422                 default:
423                     // This should not happen
424                     logger.warn("Unknown channel id {} in onecall minutely weather data", channelId);
425                     break;
426             }
427             logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
428             updateState(channelUID, state);
429         } else {
430             logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
431         }
432     }
433
434     private void updateMinutelyForecastTimeSeries(ChannelUID channelUID) {
435         String channelId = channelUID.getIdWithoutGroup();
436         String channelGroupId = channelUID.getGroupId();
437         OpenWeatherMapOneCallAPIData localWeatherData = weatherData;
438         if (channelId.equals(CHANNEL_TIME_STAMP)) {
439             logger.debug("Channel `{}` of group '{}' is no supported time-series channel.", channelId, channelGroupId);
440             return;
441         }
442         if (localWeatherData != null && !localWeatherData.getMinutely().isEmpty()) {
443             List<org.openhab.binding.openweathermap.internal.dto.onecall.Minutely> forecastData = localWeatherData
444                     .getMinutely();
445             TimeSeries timeSeries = new TimeSeries(REPLACE);
446             forecastData.forEach((m) -> {
447                 if (channelId.equals(CHANNEL_PRECIPITATION)) {
448                     State state = UnDefType.UNDEF;
449                     Instant timestamp = Instant.ofEpochSecond(m.getDt());
450                     double precipitation = m.getPrecipitation();
451                     state = getQuantityTypeState(precipitation, MILLI(METRE));
452                     timeSeries.add(timestamp, state);
453                 } else {
454                     // This should not happen
455                     logger.warn("Unknown channel id {} in onecall minutely weather data", channelId);
456                     return;
457                 }
458             });
459             logger.debug("Update channel '{}' of group '{}' with new time-series '{}'.", channelId, channelGroupId,
460                     timeSeries);
461             sendTimeSeries(channelUID, timeSeries);
462         } else {
463             logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
464         }
465     }
466
467     /**
468      * Update the hourly forecast channel from the last OpenWeatherMap data retrieved.
469      *
470      * @param channelUID the id identifying the channel to be updated
471      * @param count the index of the hourly data referenced by the channel (hour 1 is count 0)
472      */
473     private void updateHourlyForecastChannel(ChannelUID channelUID, int count) {
474         String channelId = channelUID.getIdWithoutGroup();
475         String channelGroupId = channelUID.getGroupId();
476         if (forecastHours == 0) {
477             logger.warn(
478                     "Can't update channel group {} because forecastHours is set to '0'. Please adjust config accordingly",
479                     channelGroupId);
480             return;
481         }
482         OpenWeatherMapOneCallAPIData localWeatherData = weatherData;
483         if (localWeatherData != null && localWeatherData.getHourly().size() > count) {
484             org.openhab.binding.openweathermap.internal.dto.onecall.Hourly forecastData = localWeatherData.getHourly()
485                     .get(count);
486             State state = getHourlyForecastState(channelId, forecastData, localWeatherData);
487             logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
488             updateState(channelUID, state);
489         } else {
490             logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
491         }
492     }
493
494     /**
495      * Update the hourly forecast time series channel from the last OpenWeatherMap data retrieved.
496      * 
497      * @param channelUID the id identifying the channel to be updated
498      */
499     private void updateHourlyForecastTimeSeries(ChannelUID channelUID) {
500         String channelId = channelUID.getIdWithoutGroup();
501         String channelGroupId = channelUID.getGroupId();
502         if (channelId.equals(CHANNEL_TIME_STAMP)) {
503             logger.debug("Channel `{}` of group '{}' is no supported time-series channel.", channelId, channelGroupId);
504             return;
505         }
506         OpenWeatherMapOneCallAPIData localWeatherData = weatherData;
507         if (localWeatherData != null && !localWeatherData.getHourly().isEmpty()) {
508             List<org.openhab.binding.openweathermap.internal.dto.onecall.Hourly> forecastData = localWeatherData
509                     .getHourly();
510             TimeSeries timeSeries = new TimeSeries(REPLACE);
511             forecastData.forEach((h) -> {
512                 Instant timestamp = Instant.ofEpochSecond(h.getDt());
513                 State state = getHourlyForecastState(channelId, h, localWeatherData);
514                 timeSeries.add(timestamp, state);
515             });
516             logger.debug("Update channel '{}' of group '{}' with new time-series '{}'.", channelId, channelGroupId,
517                     timeSeries);
518             sendTimeSeries(channelUID, timeSeries);
519         } else {
520             logger.debug("No weather data available to update channel '{}'.", channelId);
521         }
522     }
523
524     private State getHourlyForecastState(String channelId, Hourly forecastData,
525             OpenWeatherMapOneCallAPIData localWeatherData) {
526         State state = UnDefType.UNDEF;
527         switch (channelId) {
528             case CHANNEL_TIME_STAMP:
529                 state = getDateTimeTypeState(forecastData.getDt());
530                 break;
531             case CHANNEL_CONDITION:
532                 if (!forecastData.getWeather().isEmpty()) {
533                     state = getStringTypeState(forecastData.getWeather().get(0).getDescription());
534                 }
535                 break;
536             case CHANNEL_CONDITION_ID:
537                 if (!forecastData.getWeather().isEmpty()) {
538                     state = getStringTypeState(Integer.toString(forecastData.getWeather().get(0).getId()));
539                 }
540                 break;
541             case CHANNEL_CONDITION_ICON:
542                 if (!forecastData.getWeather().isEmpty()) {
543                     state = getRawTypeState(
544                             OpenWeatherMapConnection.getWeatherIcon(forecastData.getWeather().get(0).getIcon()));
545                 }
546                 break;
547             case CHANNEL_CONDITION_ICON_ID:
548                 if (!forecastData.getWeather().isEmpty()) {
549                     state = getStringTypeState(forecastData.getWeather().get(0).getIcon());
550                 }
551                 break;
552             case CHANNEL_TEMPERATURE:
553                 state = getQuantityTypeState(forecastData.getTemp(), CELSIUS);
554                 break;
555             case CHANNEL_APPARENT_TEMPERATURE:
556                 state = getQuantityTypeState(forecastData.getFeelsLike(), CELSIUS);
557                 break;
558             case CHANNEL_PRESSURE:
559                 state = getQuantityTypeState(forecastData.getPressure(), HECTO(PASCAL));
560                 break;
561             case CHANNEL_HUMIDITY:
562                 state = getQuantityTypeState(forecastData.getHumidity(), PERCENT);
563                 break;
564             case CHANNEL_DEW_POINT:
565                 state = getQuantityTypeState(forecastData.getDewPoint(), CELSIUS);
566                 break;
567             case CHANNEL_WIND_SPEED:
568                 state = getQuantityTypeState(forecastData.getWindSpeed(), METRE_PER_SECOND);
569                 break;
570             case CHANNEL_WIND_DIRECTION:
571                 state = getQuantityTypeState(forecastData.getWindDeg(), DEGREE_ANGLE);
572                 break;
573             case CHANNEL_GUST_SPEED:
574                 state = getQuantityTypeState(forecastData.getWindGust(), METRE_PER_SECOND);
575                 break;
576             case CHANNEL_CLOUDINESS:
577                 state = getQuantityTypeState(forecastData.getClouds(), PERCENT);
578                 break;
579             case CHANNEL_VISIBILITY:
580                 State tempstate = new QuantityType<>(localWeatherData.getCurrent().getVisibility(), METRE)
581                         .toUnit(KILO(METRE));
582                 state = (tempstate == null ? state : tempstate);
583             case CHANNEL_PRECIP_PROBABILITY:
584                 state = getQuantityTypeState(forecastData.getPop() * 100.0, PERCENT);
585                 break;
586             case CHANNEL_RAIN:
587                 Precipitation rain = forecastData.getRain();
588                 state = getQuantityTypeState(rain == null ? 0 : rain.get1h(), MILLI(METRE));
589                 break;
590             case CHANNEL_SNOW:
591                 Precipitation snow = forecastData.getSnow();
592                 state = getQuantityTypeState(snow == null ? 0 : snow.get1h(), MILLI(METRE));
593                 break;
594             default:
595                 // This should not happen
596                 logger.warn("Unknown channel id {} in OneCall hourly weather data", channelId);
597                 break;
598         }
599         return state;
600     }
601
602     /**
603      * Update the daily forecast channel from the last OpenWeatherMap data retrieved.
604      *
605      * @param channelUID the id identifying the channel to be updated
606      * @param count the index of the daily data referenced by the channel (today is count 0)
607      */
608     private void updateDailyForecastChannel(ChannelUID channelUID, int count) {
609         String channelId = channelUID.getIdWithoutGroup();
610         String channelGroupId = channelUID.getGroupId();
611         if (forecastDays == 0) {
612             logger.warn(
613                     "Can't update channel group {} because forecastDays is set to '0'. Please adjust config accordingly",
614                     channelGroupId);
615             return;
616         }
617         OpenWeatherMapOneCallAPIData localWeatherData = weatherData;
618         if (localWeatherData != null && localWeatherData.getDaily().size() > count) {
619             org.openhab.binding.openweathermap.internal.dto.onecall.Daily forecastData = localWeatherData.getDaily()
620                     .get(count);
621             State state = getDailyForecastState(channelId, forecastData, localWeatherData);
622             logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
623             updateState(channelUID, state);
624         } else {
625             logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
626         }
627     }
628
629     private void updateDailyForecastTimeSeries(ChannelUID channelUID) {
630         String channelId = channelUID.getIdWithoutGroup();
631         String channelGroupId = channelUID.getGroupId();
632         if (channelId.equals(CHANNEL_TIME_STAMP)) {
633             logger.debug("Channel `{}` of group '{}' is no supported time-series channel.", channelId, channelGroupId);
634             return;
635         }
636         OpenWeatherMapOneCallAPIData localWeatherData = weatherData;
637         if (localWeatherData != null && !localWeatherData.getDaily().isEmpty()) {
638             List<org.openhab.binding.openweathermap.internal.dto.onecall.Daily> forecastData = localWeatherData
639                     .getDaily();
640             TimeSeries timeSeries = new TimeSeries(REPLACE);
641             forecastData.forEach((d) -> {
642                 Instant timestamp = Instant.ofEpochSecond(d.getDt());
643                 State state = getDailyForecastState(channelId, d, localWeatherData);
644                 timeSeries.add(timestamp, state);
645             });
646             logger.debug("Update channel '{}' of group '{}' with new time-series '{}'.", channelId, channelGroupId,
647                     timeSeries);
648             sendTimeSeries(channelUID, timeSeries);
649         } else {
650             logger.debug("No weather data available to update channel '{}'.", channelId);
651         }
652     }
653
654     private State getDailyForecastState(String channelId, Daily forecastData,
655             OpenWeatherMapOneCallAPIData localWeatherData) {
656         State state = UnDefType.UNDEF;
657         FeelsLikeTemp feelsLike;
658         Temp temp;
659         switch (channelId) {
660             case CHANNEL_TIME_STAMP:
661                 state = getDateTimeTypeState(forecastData.getDt());
662                 break;
663             case CHANNEL_SUNRISE:
664                 state = getDateTimeTypeState(forecastData.getSunrise());
665                 break;
666             case CHANNEL_SUNSET:
667                 state = getDateTimeTypeState(forecastData.getSunset());
668                 break;
669             case CHANNEL_MOONRISE:
670                 state = getDateTimeTypeState(forecastData.getMoonrise());
671                 break;
672             case CHANNEL_MOONSET:
673                 state = getDateTimeTypeState(forecastData.getMoonset());
674                 break;
675             case CHANNEL_MOON_PHASE:
676                 state = getDecimalTypeState(forecastData.getMoonPhase());
677                 break;
678             case CHANNEL_CONDITION:
679                 if (!forecastData.getWeather().isEmpty()) {
680                     state = getStringTypeState(forecastData.getWeather().get(0).getDescription());
681                 }
682                 break;
683             case CHANNEL_CONDITION_ID:
684                 if (!forecastData.getWeather().isEmpty()) {
685                     state = getStringTypeState(Integer.toString(forecastData.getWeather().get(0).getId()));
686                 }
687                 break;
688             case CHANNEL_CONDITION_ICON:
689                 if (!forecastData.getWeather().isEmpty()) {
690                     state = getRawTypeState(
691                             OpenWeatherMapConnection.getWeatherIcon(forecastData.getWeather().get(0).getIcon()));
692                 }
693                 break;
694             case CHANNEL_CONDITION_ICON_ID:
695                 if (!forecastData.getWeather().isEmpty()) {
696                     state = getStringTypeState(forecastData.getWeather().get(0).getIcon());
697                 }
698                 break;
699             case CHANNEL_MIN_TEMPERATURE:
700                 temp = forecastData.getTemp();
701                 if (temp != null) {
702                     state = getQuantityTypeState(temp.getMin(), CELSIUS);
703                 }
704                 break;
705             case CHANNEL_MAX_TEMPERATURE:
706                 temp = forecastData.getTemp();
707                 if (temp != null) {
708                     state = getQuantityTypeState(temp.getMax(), CELSIUS);
709                 }
710                 break;
711             case CHANNEL_MORNING_TEMPERATURE:
712                 temp = forecastData.getTemp();
713                 if (temp != null) {
714                     state = getQuantityTypeState(temp.getMorn(), CELSIUS);
715                 }
716                 break;
717             case CHANNEL_DAY_TEMPERATURE:
718                 temp = forecastData.getTemp();
719                 if (temp != null) {
720                     state = getQuantityTypeState(temp.getDay(), CELSIUS);
721                 }
722                 break;
723             case CHANNEL_EVENING_TEMPERATURE:
724                 temp = forecastData.getTemp();
725                 if (temp != null) {
726                     state = getQuantityTypeState(temp.getEve(), CELSIUS);
727                 }
728                 break;
729             case CHANNEL_NIGHT_TEMPERATURE:
730                 temp = forecastData.getTemp();
731                 if (temp != null) {
732                     state = getQuantityTypeState(temp.getNight(), CELSIUS);
733                 }
734                 break;
735
736             case CHANNEL_APPARENT_DAY:
737                 feelsLike = forecastData.getFeelsLike();
738                 if (feelsLike != null) {
739                     state = getQuantityTypeState(feelsLike.getDay(), CELSIUS);
740                 }
741                 break;
742             case CHANNEL_APPARENT_MORNING:
743                 feelsLike = forecastData.getFeelsLike();
744                 if (feelsLike != null) {
745                     state = getQuantityTypeState(feelsLike.getMorn(), CELSIUS);
746                 }
747                 break;
748             case CHANNEL_APPARENT_EVENING:
749                 feelsLike = forecastData.getFeelsLike();
750                 if (feelsLike != null) {
751                     state = getQuantityTypeState(feelsLike.getEve(), CELSIUS);
752                 }
753                 break;
754             case CHANNEL_APPARENT_NIGHT:
755                 feelsLike = forecastData.getFeelsLike();
756                 if (feelsLike != null) {
757                     state = getQuantityTypeState(feelsLike.getNight(), CELSIUS);
758                 }
759                 break;
760             case CHANNEL_PRESSURE:
761                 state = getQuantityTypeState(forecastData.getPressure(), HECTO(PASCAL));
762                 break;
763             case CHANNEL_HUMIDITY:
764                 state = getQuantityTypeState(forecastData.getHumidity(), PERCENT);
765                 break;
766             case CHANNEL_WIND_SPEED:
767                 state = getQuantityTypeState(forecastData.getWindSpeed(), METRE_PER_SECOND);
768                 break;
769             case CHANNEL_WIND_DIRECTION:
770                 state = getQuantityTypeState(forecastData.getWindDeg(), DEGREE_ANGLE);
771                 break;
772             case CHANNEL_GUST_SPEED:
773                 state = getQuantityTypeState(forecastData.getWindGust(), METRE_PER_SECOND);
774                 break;
775             case CHANNEL_CLOUDINESS:
776                 state = getQuantityTypeState(forecastData.getClouds(), PERCENT);
777                 break;
778             case CHANNEL_DEW_POINT:
779                 state = getQuantityTypeState(forecastData.getDewPoint(), CELSIUS);
780                 break;
781             case CHANNEL_UVINDEX:
782                 state = getDecimalTypeState(forecastData.getUvi());
783                 break;
784             case CHANNEL_VISIBILITY:
785                 State tempstate = new QuantityType<>(localWeatherData.getCurrent().getVisibility(), METRE)
786                         .toUnit(KILO(METRE));
787                 state = (tempstate == null ? state : tempstate);
788             case CHANNEL_PRECIP_PROBABILITY:
789                 state = getQuantityTypeState(forecastData.getPop() * 100.0, PERCENT);
790                 break;
791             case CHANNEL_RAIN:
792                 state = getQuantityTypeState(forecastData.getRain(), MILLI(METRE));
793                 break;
794             case CHANNEL_SNOW:
795                 state = getQuantityTypeState(forecastData.getSnow(), MILLI(METRE));
796                 break;
797             default:
798                 // This should not happen
799                 logger.warn("Unknown channel id {} in OneCall daily weather data", channelId);
800                 break;
801         }
802         return state;
803     }
804
805     /**
806      * Update the channel from the last OpenWeaterhMap data retrieved.
807      *
808      * @param channelUID the id identifying the channel to be updated
809      * @param count the index of the alert data referenced by the channel (alert 1 is count 0)
810      */
811     private void updateAlertsChannel(ChannelUID channelUID, int count) {
812         String channelId = channelUID.getIdWithoutGroup();
813         String channelGroupId = channelUID.getGroupId();
814         OpenWeatherMapOneCallAPIData localWeatherData = weatherData;
815         List<Alert> alerts = localWeatherData != null ? localWeatherData.alerts : null;
816         State state = UnDefType.UNDEF;
817         if (alerts != null && alerts.size() > count) {
818             Alert alert = alerts.get(count);
819             switch (channelId) {
820                 case CHANNEL_ALERT_EVENT:
821                     state = getStringTypeState(alert.getEvent());
822                     break;
823                 case CHANNEL_ALERT_DESCRIPTION:
824                     state = getStringTypeState(alert.getDescription());
825                     break;
826                 case CHANNEL_ALERT_ONSET:
827                     state = getDateTimeTypeState(alert.getStart());
828                     break;
829                 case CHANNEL_ALERT_EXPIRES:
830                     state = getDateTimeTypeState(alert.getEnd());
831                     break;
832                 case CHANNEL_ALERT_SOURCE:
833                     state = getStringTypeState(alert.getSenderName());
834                     break;
835             }
836             logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
837         } else {
838             logger.debug("No data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
839         }
840         updateState(channelUID, state);
841     }
842 }