2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.openweathermap.internal.handler;
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.*;
20 import java.util.ArrayList;
21 import java.util.List;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24 import java.util.stream.Collectors;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpResponseException;
29 import org.openhab.binding.openweathermap.internal.config.OpenWeatherMapWeatherAndForecastConfiguration;
30 import org.openhab.binding.openweathermap.internal.connection.OpenWeatherMapConnection;
31 import org.openhab.binding.openweathermap.internal.dto.OpenWeatherMapJsonDailyForecastData;
32 import org.openhab.binding.openweathermap.internal.dto.OpenWeatherMapJsonHourlyForecastData;
33 import org.openhab.binding.openweathermap.internal.dto.OpenWeatherMapJsonWeatherData;
34 import org.openhab.binding.openweathermap.internal.dto.base.Rain;
35 import org.openhab.binding.openweathermap.internal.dto.base.Snow;
36 import org.openhab.binding.openweathermap.internal.dto.forecast.daily.FeelsLikeTemp;
37 import org.openhab.core.config.core.Configuration;
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.UnDefType;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
53 import com.google.gson.JsonSyntaxException;
56 * The {@link OpenWeatherMapWeatherAndForecastHandler} is responsible for handling commands, which are sent to one of
59 * @author Christoph Weitkamp - Initial contribution
62 public class OpenWeatherMapWeatherAndForecastHandler extends AbstractOpenWeatherMapHandler {
64 private final Logger logger = LoggerFactory.getLogger(OpenWeatherMapWeatherAndForecastHandler.class);
66 private static final String CHANNEL_GROUP_HOURLY_FORECAST_PREFIX = "forecastHours";
67 private static final String CHANNEL_GROUP_DAILY_FORECAST_PREFIX = "forecastDay";
68 private static final Pattern CHANNEL_GROUP_HOURLY_FORECAST_PREFIX_PATTERN = Pattern
69 .compile(CHANNEL_GROUP_HOURLY_FORECAST_PREFIX + "([0-9]*)");
70 private static final Pattern CHANNEL_GROUP_DAILY_FORECAST_PREFIX_PATTERN = Pattern
71 .compile(CHANNEL_GROUP_DAILY_FORECAST_PREFIX + "([0-9]*)");
73 // keeps track of the parsed counts
74 private int forecastHours = 24;
75 private int forecastDays = 6;
77 private @Nullable OpenWeatherMapJsonWeatherData weatherData;
78 private @Nullable OpenWeatherMapJsonHourlyForecastData hourlyForecastData;
79 private @Nullable OpenWeatherMapJsonDailyForecastData dailyForecastData;
81 public OpenWeatherMapWeatherAndForecastHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
82 super(thing, timeZoneProvider);
86 public void initialize() {
88 logger.debug("Initialize OpenWeatherMapWeatherAndForecastHandler handler '{}'.", getThing().getUID());
89 OpenWeatherMapWeatherAndForecastConfiguration config = getConfigAs(
90 OpenWeatherMapWeatherAndForecastConfiguration.class);
92 boolean configValid = true;
93 int newForecastHours = config.forecastHours;
94 if (newForecastHours < 0 || newForecastHours > 120 || newForecastHours % 3 != 0) {
95 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
96 "@text/offline.conf-error-not-supported-number-of-hours");
99 int newForecastDays = config.forecastDays;
100 if (newForecastDays < 0 || newForecastDays > 16) {
101 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
102 "@text/offline.conf-error-not-supported-number-of-days");
107 logger.debug("Rebuilding thing '{}'.", getThing().getUID());
108 List<Channel> toBeAddedChannels = new ArrayList<>();
109 List<Channel> toBeRemovedChannels = new ArrayList<>();
110 if (forecastHours != newForecastHours) {
111 logger.debug("Rebuilding hourly forecast channel groups.");
112 if (forecastHours > newForecastHours) {
113 for (int i = newForecastHours + 3; i <= forecastHours; i += 3) {
114 toBeRemovedChannels.addAll(removeChannelsOfGroup(
115 CHANNEL_GROUP_HOURLY_FORECAST_PREFIX + ((i < 10) ? "0" : "") + Integer.toString(i)));
118 for (int i = forecastHours + 3; i <= newForecastHours; i += 3) {
119 toBeAddedChannels.addAll(createChannelsForGroup(
120 CHANNEL_GROUP_HOURLY_FORECAST_PREFIX + ((i < 10) ? "0" : "") + Integer.toString(i),
121 CHANNEL_GROUP_TYPE_HOURLY_FORECAST));
124 forecastHours = newForecastHours;
126 if (forecastDays != newForecastDays) {
127 logger.debug("Rebuilding daily forecast channel groups.");
128 if (forecastDays > newForecastDays) {
129 if (newForecastDays < 1) {
130 toBeRemovedChannels.addAll(removeChannelsOfGroup(CHANNEL_GROUP_FORECAST_TODAY));
132 if (newForecastDays < 2) {
133 toBeRemovedChannels.addAll(removeChannelsOfGroup(CHANNEL_GROUP_FORECAST_TOMORROW));
135 for (int i = newForecastDays; i < forecastDays; ++i) {
136 toBeRemovedChannels.addAll(
137 removeChannelsOfGroup(CHANNEL_GROUP_DAILY_FORECAST_PREFIX + Integer.toString(i)));
140 if (forecastDays == 0 && newForecastDays > 0) {
141 toBeAddedChannels.addAll(createChannelsForGroup(CHANNEL_GROUP_FORECAST_TODAY,
142 CHANNEL_GROUP_TYPE_DAILY_FORECAST));
144 if (forecastDays <= 1 && newForecastDays > 1) {
145 toBeAddedChannels.addAll(createChannelsForGroup(CHANNEL_GROUP_FORECAST_TOMORROW,
146 CHANNEL_GROUP_TYPE_DAILY_FORECAST));
148 for (int i = (forecastDays < 2) ? 2 : forecastDays; i < newForecastDays; ++i) {
149 toBeAddedChannels.addAll(
150 createChannelsForGroup(CHANNEL_GROUP_DAILY_FORECAST_PREFIX + Integer.toString(i),
151 CHANNEL_GROUP_TYPE_DAILY_FORECAST));
154 forecastDays = newForecastDays;
156 ThingBuilder builder = editThing().withoutChannels(toBeRemovedChannels);
157 for (Channel channel : toBeAddedChannels) {
158 builder.withChannel(channel);
160 updateThing(builder.build());
165 protected boolean requestData(OpenWeatherMapConnection connection)
166 throws CommunicationException, ConfigurationException {
167 logger.debug("Update weather and forecast data of thing '{}'.", getThing().getUID());
169 weatherData = connection.getWeatherData(location);
170 if (forecastHours > 0) {
171 hourlyForecastData = connection.getHourlyForecastData(location, forecastHours / 3);
173 if (forecastDays > 0) {
175 dailyForecastData = connection.getDailyForecastData(location, forecastDays);
176 } catch (ConfigurationException e) {
177 if (e.getCause() instanceof HttpResponseException) {
179 Configuration editConfig = editConfiguration();
180 editConfig.put(CONFIG_FORECAST_DAYS, 0);
181 updateConfiguration(editConfig);
182 logger.debug("Removing daily forecast channel groups.");
183 List<Channel> channels = getThing().getChannels().stream()
184 .filter(c -> CHANNEL_GROUP_FORECAST_TODAY.equals(c.getUID().getGroupId())
185 || CHANNEL_GROUP_FORECAST_TOMORROW.equals(c.getUID().getGroupId())
186 || c.getUID().getGroupId().startsWith(CHANNEL_GROUP_DAILY_FORECAST_PREFIX))
187 .collect(Collectors.toList());
188 updateThing(editThing().withoutChannels(channels).build());
195 } catch (JsonSyntaxException e) {
196 logger.debug("JsonSyntaxException occurred during execution: {}", e.getMessage(), e);
202 protected void updateChannel(ChannelUID channelUID) {
203 String channelGroupId = channelUID.getGroupId();
204 switch (channelGroupId) {
205 case CHANNEL_GROUP_STATION:
206 case CHANNEL_GROUP_CURRENT_WEATHER:
207 updateCurrentChannel(channelUID);
209 case CHANNEL_GROUP_FORECAST_TODAY:
210 updateDailyForecastChannel(channelUID, 0);
212 case CHANNEL_GROUP_FORECAST_TOMORROW:
213 updateDailyForecastChannel(channelUID, 1);
217 Matcher hourlyForecastMatcher = CHANNEL_GROUP_HOURLY_FORECAST_PREFIX_PATTERN.matcher(channelGroupId);
218 if (hourlyForecastMatcher.find() && (i = Integer.parseInt(hourlyForecastMatcher.group(1))) >= 3
220 updateHourlyForecastChannel(channelUID, (i / 3) - 1);
223 Matcher dailyForecastMatcher = CHANNEL_GROUP_DAILY_FORECAST_PREFIX_PATTERN.matcher(channelGroupId);
224 if (dailyForecastMatcher.find() && (i = Integer.parseInt(dailyForecastMatcher.group(1))) > 1
226 updateDailyForecastChannel(channelUID, i);
234 * Update the channel from the last OpenWeatherMap data retrieved.
236 * @param channelUID the id identifying the channel to be updated
238 private void updateCurrentChannel(ChannelUID channelUID) {
239 String channelId = channelUID.getIdWithoutGroup();
240 String channelGroupId = channelUID.getGroupId();
241 OpenWeatherMapJsonWeatherData localWeatherData = weatherData;
242 if (localWeatherData != null) {
243 State state = UnDefType.UNDEF;
245 case CHANNEL_STATION_ID:
246 state = getStringTypeState(localWeatherData.getId().toString());
248 case CHANNEL_STATION_NAME:
249 state = getStringTypeState(localWeatherData.getName());
251 case CHANNEL_STATION_LOCATION:
252 state = getPointTypeState(localWeatherData.getCoord().getLat(),
253 localWeatherData.getCoord().getLon());
255 case CHANNEL_TIME_STAMP:
256 state = getDateTimeTypeState(localWeatherData.getDt());
258 case CHANNEL_CONDITION:
259 if (!localWeatherData.getWeather().isEmpty()) {
260 state = getStringTypeState(localWeatherData.getWeather().get(0).getDescription());
263 case CHANNEL_CONDITION_ID:
264 if (!localWeatherData.getWeather().isEmpty()) {
265 state = getStringTypeState(localWeatherData.getWeather().get(0).getId().toString());
268 case CHANNEL_CONDITION_ICON:
269 if (!localWeatherData.getWeather().isEmpty()) {
270 state = getRawTypeState(OpenWeatherMapConnection
271 .getWeatherIcon(localWeatherData.getWeather().get(0).getIcon()));
274 case CHANNEL_CONDITION_ICON_ID:
275 if (!localWeatherData.getWeather().isEmpty()) {
276 state = getStringTypeState(localWeatherData.getWeather().get(0).getIcon());
279 case CHANNEL_TEMPERATURE:
280 state = getQuantityTypeState(localWeatherData.getMain().getTemp(), CELSIUS);
282 case CHANNEL_APPARENT_TEMPERATURE:
283 state = getQuantityTypeState(localWeatherData.getMain().getFeelsLikeTemp(), CELSIUS);
285 case CHANNEL_PRESSURE:
286 state = getQuantityTypeState(localWeatherData.getMain().getPressure(), HECTO(PASCAL));
288 case CHANNEL_HUMIDITY:
289 state = getQuantityTypeState(localWeatherData.getMain().getHumidity(), PERCENT);
291 case CHANNEL_WIND_SPEED:
292 state = getQuantityTypeState(localWeatherData.getWind().getSpeed(), METRE_PER_SECOND);
294 case CHANNEL_WIND_DIRECTION:
295 state = getQuantityTypeState(localWeatherData.getWind().getDeg(), DEGREE_ANGLE);
297 case CHANNEL_GUST_SPEED:
298 state = getQuantityTypeState(localWeatherData.getWind().getGust(), METRE_PER_SECOND);
300 case CHANNEL_CLOUDINESS:
301 state = getQuantityTypeState(localWeatherData.getClouds().getAll(), PERCENT);
304 Rain rain = localWeatherData.getRain();
305 state = getQuantityTypeState(rain == null ? 0 : rain.getVolume(), MILLI(METRE));
308 Snow snow = localWeatherData.getSnow();
309 state = getQuantityTypeState(snow == null ? 0 : snow.getVolume(), MILLI(METRE));
311 case CHANNEL_VISIBILITY:
312 Integer localVisibility = localWeatherData.getVisibility();
313 state = localVisibility == null ? UnDefType.UNDEF
314 : new QuantityType<>(localVisibility, METRE).toUnit(KILO(METRE));
316 logger.debug("State conversion failed, cannot update state.");
321 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
322 updateState(channelUID, state);
324 logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
329 * Update the channel from the last OpenWeatherMap data retrieved.
331 * @param channelUID the id identifying the channel to be updated
334 private void updateHourlyForecastChannel(ChannelUID channelUID, int count) {
335 String channelId = channelUID.getIdWithoutGroup();
336 String channelGroupId = channelUID.getGroupId();
337 OpenWeatherMapJsonHourlyForecastData localHourlyForecastData = hourlyForecastData;
338 if (localHourlyForecastData != null && localHourlyForecastData.getList().size() > count) {
339 org.openhab.binding.openweathermap.internal.dto.forecast.hourly.List forecastData = localHourlyForecastData
340 .getList().get(count);
341 State state = UnDefType.UNDEF;
343 case CHANNEL_TIME_STAMP:
344 state = getDateTimeTypeState(forecastData.getDt());
346 case CHANNEL_CONDITION:
347 if (!forecastData.getWeather().isEmpty()) {
348 state = getStringTypeState(forecastData.getWeather().get(0).getDescription());
351 case CHANNEL_CONDITION_ID:
352 if (!forecastData.getWeather().isEmpty()) {
353 state = getStringTypeState(forecastData.getWeather().get(0).getId().toString());
356 case CHANNEL_CONDITION_ICON:
357 if (!forecastData.getWeather().isEmpty()) {
358 state = getRawTypeState(
359 OpenWeatherMapConnection.getWeatherIcon(forecastData.getWeather().get(0).getIcon()));
362 case CHANNEL_CONDITION_ICON_ID:
363 if (!forecastData.getWeather().isEmpty()) {
364 state = getStringTypeState(forecastData.getWeather().get(0).getIcon());
367 case CHANNEL_TEMPERATURE:
368 state = getQuantityTypeState(forecastData.getMain().getTemp(), CELSIUS);
370 case CHANNEL_APPARENT_TEMPERATURE:
371 state = getQuantityTypeState(forecastData.getMain().getFeelsLikeTemp(), CELSIUS);
373 case CHANNEL_MIN_TEMPERATURE:
374 state = getQuantityTypeState(forecastData.getMain().getTempMin(), CELSIUS);
376 case CHANNEL_MAX_TEMPERATURE:
377 state = getQuantityTypeState(forecastData.getMain().getTempMax(), CELSIUS);
379 case CHANNEL_PRESSURE:
380 state = getQuantityTypeState(forecastData.getMain().getPressure(), HECTO(PASCAL));
382 case CHANNEL_HUMIDITY:
383 state = getQuantityTypeState(forecastData.getMain().getHumidity(), PERCENT);
385 case CHANNEL_WIND_SPEED:
386 state = getQuantityTypeState(forecastData.getWind().getSpeed(), METRE_PER_SECOND);
388 case CHANNEL_WIND_DIRECTION:
389 state = getQuantityTypeState(forecastData.getWind().getDeg(), DEGREE_ANGLE);
391 case CHANNEL_GUST_SPEED:
392 state = getQuantityTypeState(forecastData.getWind().getGust(), METRE_PER_SECOND);
394 case CHANNEL_CLOUDINESS:
395 state = getQuantityTypeState(forecastData.getClouds().getAll(), PERCENT);
398 Rain rain = forecastData.getRain();
399 state = getQuantityTypeState(rain == null ? 0 : rain.getVolume(), MILLI(METRE));
402 Snow snow = forecastData.getSnow();
403 state = getQuantityTypeState(snow == null ? 0 : snow.getVolume(), MILLI(METRE));
406 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
407 updateState(channelUID, state);
409 logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
414 * Update the channel from the last OpenWeatherMap data retrieved.
416 * @param channelUID the id identifying the channel to be updated
419 private void updateDailyForecastChannel(ChannelUID channelUID, int count) {
420 String channelId = channelUID.getIdWithoutGroup();
421 String channelGroupId = channelUID.getGroupId();
422 OpenWeatherMapJsonDailyForecastData localDailyForecastData = dailyForecastData;
423 if (localDailyForecastData != null && localDailyForecastData.getList().size() > count) {
424 org.openhab.binding.openweathermap.internal.dto.forecast.daily.List forecastData = localDailyForecastData
425 .getList().get(count);
426 State state = UnDefType.UNDEF;
428 case CHANNEL_TIME_STAMP:
429 state = getDateTimeTypeState(forecastData.getDt());
431 case CHANNEL_SUNRISE:
432 state = getDateTimeTypeState(forecastData.getSunrise());
435 state = getDateTimeTypeState(forecastData.getSunset());
437 case CHANNEL_CONDITION:
438 if (!forecastData.getWeather().isEmpty()) {
439 state = getStringTypeState(forecastData.getWeather().get(0).getDescription());
442 case CHANNEL_CONDITION_ID:
443 if (!forecastData.getWeather().isEmpty()) {
444 state = getStringTypeState(forecastData.getWeather().get(0).getId().toString());
447 case CHANNEL_CONDITION_ICON:
448 if (!forecastData.getWeather().isEmpty()) {
449 state = getRawTypeState(
450 OpenWeatherMapConnection.getWeatherIcon(forecastData.getWeather().get(0).getIcon()));
453 case CHANNEL_CONDITION_ICON_ID:
454 if (!forecastData.getWeather().isEmpty()) {
455 state = getStringTypeState(forecastData.getWeather().get(0).getIcon());
458 case CHANNEL_MIN_TEMPERATURE:
459 state = getQuantityTypeState(forecastData.getTemp().getMin(), CELSIUS);
461 case CHANNEL_MAX_TEMPERATURE:
462 state = getQuantityTypeState(forecastData.getTemp().getMax(), CELSIUS);
464 case CHANNEL_APPARENT_TEMPERATURE:
465 FeelsLikeTemp feelsLikeTemp = forecastData.getFeelsLikeTemp();
466 if (feelsLikeTemp != null) {
467 state = getQuantityTypeState(feelsLikeTemp.getDay(), CELSIUS);
470 case CHANNEL_PRESSURE:
471 state = getQuantityTypeState(forecastData.getPressure(), HECTO(PASCAL));
473 case CHANNEL_HUMIDITY:
474 state = getQuantityTypeState(forecastData.getHumidity(), PERCENT);
476 case CHANNEL_WIND_SPEED:
477 state = getQuantityTypeState(forecastData.getSpeed(), METRE_PER_SECOND);
479 case CHANNEL_WIND_DIRECTION:
480 state = getQuantityTypeState(forecastData.getDeg(), DEGREE_ANGLE);
482 case CHANNEL_GUST_SPEED:
483 state = getQuantityTypeState(forecastData.getGust(), METRE_PER_SECOND);
485 case CHANNEL_CLOUDINESS:
486 state = getQuantityTypeState(forecastData.getClouds(), PERCENT);
489 Double rain = forecastData.getRain();
490 state = getQuantityTypeState(rain == null ? 0 : rain, MILLI(METRE));
493 Double snow = forecastData.getSnow();
494 state = getQuantityTypeState(snow == null ? 0 : snow, MILLI(METRE));
496 case CHANNEL_PRECIP_PROBABILITY:
497 Double probability = forecastData.getPop();
498 state = getQuantityTypeState(probability == null ? 0 : probability * 100.0, PERCENT);
501 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
502 updateState(channelUID, state);
504 logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);