2 * Copyright (c) 2010-2024 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.Precipitation;
35 import org.openhab.binding.openweathermap.internal.dto.forecast.daily.FeelsLikeTemp;
36 import org.openhab.core.config.core.Configuration;
37 import org.openhab.core.i18n.CommunicationException;
38 import org.openhab.core.i18n.ConfigurationException;
39 import org.openhab.core.i18n.TimeZoneProvider;
40 import org.openhab.core.library.types.QuantityType;
41 import org.openhab.core.thing.Channel;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.binding.builder.ThingBuilder;
47 import org.openhab.core.types.State;
48 import org.openhab.core.types.UnDefType;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
52 import com.google.gson.JsonSyntaxException;
55 * The {@link OpenWeatherMapWeatherAndForecastHandler} is responsible for handling commands, which are sent to one of
58 * @author Christoph Weitkamp - Initial contribution
61 public class OpenWeatherMapWeatherAndForecastHandler extends AbstractOpenWeatherMapHandler {
63 private final Logger logger = LoggerFactory.getLogger(OpenWeatherMapWeatherAndForecastHandler.class);
65 private static final String CHANNEL_GROUP_HOURLY_FORECAST_PREFIX = "forecastHours";
66 private static final String CHANNEL_GROUP_DAILY_FORECAST_PREFIX = "forecastDay";
67 private static final Pattern CHANNEL_GROUP_HOURLY_FORECAST_PREFIX_PATTERN = Pattern
68 .compile(CHANNEL_GROUP_HOURLY_FORECAST_PREFIX + "([0-9]*)");
69 private static final Pattern CHANNEL_GROUP_DAILY_FORECAST_PREFIX_PATTERN = Pattern
70 .compile(CHANNEL_GROUP_DAILY_FORECAST_PREFIX + "([0-9]*)");
72 // keeps track of the parsed counts
73 private int forecastHours = 24;
74 private int forecastDays = 6;
76 private @Nullable OpenWeatherMapJsonWeatherData weatherData;
77 private @Nullable OpenWeatherMapJsonHourlyForecastData hourlyForecastData;
78 private @Nullable OpenWeatherMapJsonDailyForecastData dailyForecastData;
80 public OpenWeatherMapWeatherAndForecastHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
81 super(thing, timeZoneProvider);
85 public void initialize() {
87 logger.debug("Initialize OpenWeatherMapWeatherAndForecastHandler handler '{}'.", getThing().getUID());
88 OpenWeatherMapWeatherAndForecastConfiguration config = getConfigAs(
89 OpenWeatherMapWeatherAndForecastConfiguration.class);
91 boolean configValid = true;
92 int newForecastHours = config.forecastHours;
93 if (newForecastHours < 0 || newForecastHours > 120 || newForecastHours % 3 != 0) {
94 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
95 "@text/offline.conf-error-not-supported-number-of-hours");
98 int newForecastDays = config.forecastDays;
99 if (newForecastDays < 0 || newForecastDays > 16) {
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
101 "@text/offline.conf-error-not-supported-number-of-days");
106 logger.debug("Rebuilding thing '{}'.", getThing().getUID());
107 List<Channel> toBeAddedChannels = new ArrayList<>();
108 List<Channel> toBeRemovedChannels = new ArrayList<>();
109 if (forecastHours != newForecastHours) {
110 logger.debug("Rebuilding hourly forecast channel groups.");
111 if (forecastHours > newForecastHours) {
112 for (int i = newForecastHours + 3; i <= forecastHours; i += 3) {
113 toBeRemovedChannels.addAll(removeChannelsOfGroup(
114 CHANNEL_GROUP_HOURLY_FORECAST_PREFIX + ((i < 10) ? "0" : "") + Integer.toString(i)));
117 for (int i = forecastHours + 3; i <= newForecastHours; i += 3) {
118 toBeAddedChannels.addAll(createChannelsForGroup(
119 CHANNEL_GROUP_HOURLY_FORECAST_PREFIX + ((i < 10) ? "0" : "") + Integer.toString(i),
120 CHANNEL_GROUP_TYPE_HOURLY_FORECAST));
123 forecastHours = newForecastHours;
125 if (forecastDays != newForecastDays) {
126 logger.debug("Rebuilding daily forecast channel groups.");
127 if (forecastDays > newForecastDays) {
128 if (newForecastDays < 1) {
129 toBeRemovedChannels.addAll(removeChannelsOfGroup(CHANNEL_GROUP_FORECAST_TODAY));
131 if (newForecastDays < 2) {
132 toBeRemovedChannels.addAll(removeChannelsOfGroup(CHANNEL_GROUP_FORECAST_TOMORROW));
134 for (int i = newForecastDays; i < forecastDays; ++i) {
135 toBeRemovedChannels.addAll(
136 removeChannelsOfGroup(CHANNEL_GROUP_DAILY_FORECAST_PREFIX + Integer.toString(i)));
139 if (forecastDays == 0 && newForecastDays > 0) {
140 toBeAddedChannels.addAll(createChannelsForGroup(CHANNEL_GROUP_FORECAST_TODAY,
141 CHANNEL_GROUP_TYPE_DAILY_FORECAST));
143 if (forecastDays <= 1 && newForecastDays > 1) {
144 toBeAddedChannels.addAll(createChannelsForGroup(CHANNEL_GROUP_FORECAST_TOMORROW,
145 CHANNEL_GROUP_TYPE_DAILY_FORECAST));
147 for (int i = (forecastDays < 2) ? 2 : forecastDays; i < newForecastDays; ++i) {
148 toBeAddedChannels.addAll(
149 createChannelsForGroup(CHANNEL_GROUP_DAILY_FORECAST_PREFIX + Integer.toString(i),
150 CHANNEL_GROUP_TYPE_DAILY_FORECAST));
153 forecastDays = newForecastDays;
155 ThingBuilder builder = editThing().withoutChannels(toBeRemovedChannels);
156 for (Channel channel : toBeAddedChannels) {
157 builder.withChannel(channel);
159 updateThing(builder.build());
164 protected boolean requestData(OpenWeatherMapConnection connection)
165 throws CommunicationException, ConfigurationException {
166 logger.debug("Update weather and forecast data of thing '{}'.", getThing().getUID());
168 weatherData = connection.getWeatherData(location);
169 if (forecastHours > 0) {
170 hourlyForecastData = connection.getHourlyForecastData(location, forecastHours / 3);
172 if (forecastDays > 0) {
174 dailyForecastData = connection.getDailyForecastData(location, forecastDays);
175 } catch (ConfigurationException e) {
176 if (e.getCause() instanceof HttpResponseException) {
178 Configuration editConfig = editConfiguration();
179 editConfig.put(CONFIG_FORECAST_DAYS, 0);
180 updateConfiguration(editConfig);
181 logger.debug("Removing daily forecast channel groups.");
182 List<Channel> channels = getThing().getChannels().stream()
183 .filter(c -> CHANNEL_GROUP_FORECAST_TODAY.equals(c.getUID().getGroupId())
184 || CHANNEL_GROUP_FORECAST_TOMORROW.equals(c.getUID().getGroupId())
185 || c.getUID().getGroupId().startsWith(CHANNEL_GROUP_DAILY_FORECAST_PREFIX))
186 .collect(Collectors.toList());
187 updateThing(editThing().withoutChannels(channels).build());
194 } catch (JsonSyntaxException e) {
195 logger.debug("JsonSyntaxException occurred during execution: {}", e.getMessage(), e);
201 protected void updateChannel(ChannelUID channelUID) {
202 String channelGroupId = channelUID.getGroupId();
203 switch (channelGroupId) {
204 case CHANNEL_GROUP_STATION:
205 case CHANNEL_GROUP_CURRENT_WEATHER:
206 updateCurrentChannel(channelUID);
208 case CHANNEL_GROUP_FORECAST_TODAY:
209 updateDailyForecastChannel(channelUID, 0);
211 case CHANNEL_GROUP_FORECAST_TOMORROW:
212 updateDailyForecastChannel(channelUID, 1);
216 Matcher hourlyForecastMatcher = CHANNEL_GROUP_HOURLY_FORECAST_PREFIX_PATTERN.matcher(channelGroupId);
217 if (hourlyForecastMatcher.find() && (i = Integer.parseInt(hourlyForecastMatcher.group(1))) >= 3
219 updateHourlyForecastChannel(channelUID, (i / 3) - 1);
222 Matcher dailyForecastMatcher = CHANNEL_GROUP_DAILY_FORECAST_PREFIX_PATTERN.matcher(channelGroupId);
223 if (dailyForecastMatcher.find() && (i = Integer.parseInt(dailyForecastMatcher.group(1))) > 1
225 updateDailyForecastChannel(channelUID, i);
233 * Update the channel from the last OpenWeatherMap data retrieved.
235 * @param channelUID the id identifying the channel to be updated
237 private void updateCurrentChannel(ChannelUID channelUID) {
238 String channelId = channelUID.getIdWithoutGroup();
239 String channelGroupId = channelUID.getGroupId();
240 OpenWeatherMapJsonWeatherData localWeatherData = weatherData;
241 if (localWeatherData != null) {
242 State state = UnDefType.UNDEF;
244 case CHANNEL_STATION_ID:
245 state = getStringTypeState(localWeatherData.getId().toString());
247 case CHANNEL_STATION_NAME:
248 state = getStringTypeState(localWeatherData.getName());
250 case CHANNEL_STATION_LOCATION:
251 state = getPointTypeState(localWeatherData.getCoord().getLat(),
252 localWeatherData.getCoord().getLon());
254 case CHANNEL_TIME_STAMP:
255 state = getDateTimeTypeState(localWeatherData.getDt());
257 case CHANNEL_CONDITION:
258 if (!localWeatherData.getWeather().isEmpty()) {
259 state = getStringTypeState(localWeatherData.getWeather().get(0).getDescription());
262 case CHANNEL_CONDITION_ID:
263 if (!localWeatherData.getWeather().isEmpty()) {
264 state = getStringTypeState(localWeatherData.getWeather().get(0).getId().toString());
267 case CHANNEL_CONDITION_ICON:
268 if (!localWeatherData.getWeather().isEmpty()) {
269 state = getRawTypeState(OpenWeatherMapConnection
270 .getWeatherIcon(localWeatherData.getWeather().get(0).getIcon()));
273 case CHANNEL_CONDITION_ICON_ID:
274 if (!localWeatherData.getWeather().isEmpty()) {
275 state = getStringTypeState(localWeatherData.getWeather().get(0).getIcon());
278 case CHANNEL_TEMPERATURE:
279 state = getQuantityTypeState(localWeatherData.getMain().getTemp(), CELSIUS);
281 case CHANNEL_APPARENT_TEMPERATURE:
282 state = getQuantityTypeState(localWeatherData.getMain().getFeelsLikeTemp(), CELSIUS);
284 case CHANNEL_PRESSURE:
285 state = getQuantityTypeState(localWeatherData.getMain().getPressure(), HECTO(PASCAL));
287 case CHANNEL_HUMIDITY:
288 state = getQuantityTypeState(localWeatherData.getMain().getHumidity(), PERCENT);
290 case CHANNEL_WIND_SPEED:
291 state = getQuantityTypeState(localWeatherData.getWind().getSpeed(), METRE_PER_SECOND);
293 case CHANNEL_WIND_DIRECTION:
294 state = getQuantityTypeState(localWeatherData.getWind().getDeg(), DEGREE_ANGLE);
296 case CHANNEL_GUST_SPEED:
297 state = getQuantityTypeState(localWeatherData.getWind().getGust(), METRE_PER_SECOND);
299 case CHANNEL_CLOUDINESS:
300 state = getQuantityTypeState(localWeatherData.getClouds().getAll(), PERCENT);
303 Precipitation rain = localWeatherData.getRain();
304 state = getQuantityTypeState(rain == null ? 0 : rain.getVolume(), MILLI(METRE));
307 Precipitation snow = localWeatherData.getSnow();
308 state = getQuantityTypeState(snow == null ? 0 : snow.getVolume(), MILLI(METRE));
310 case CHANNEL_VISIBILITY:
311 Integer localVisibility = localWeatherData.getVisibility();
312 state = localVisibility == null ? UnDefType.UNDEF
313 : new QuantityType<>(localVisibility, METRE).toUnit(KILO(METRE));
315 logger.debug("State conversion failed, cannot update state.");
320 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
321 updateState(channelUID, state);
323 logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
328 * Update the channel from the last OpenWeatherMap data retrieved.
330 * @param channelUID the id identifying the channel to be updated
333 private void updateHourlyForecastChannel(ChannelUID channelUID, int count) {
334 String channelId = channelUID.getIdWithoutGroup();
335 String channelGroupId = channelUID.getGroupId();
336 OpenWeatherMapJsonHourlyForecastData localHourlyForecastData = hourlyForecastData;
337 if (localHourlyForecastData != null && localHourlyForecastData.getList().size() > count) {
338 org.openhab.binding.openweathermap.internal.dto.forecast.hourly.List forecastData = localHourlyForecastData
339 .getList().get(count);
340 State state = UnDefType.UNDEF;
342 case CHANNEL_TIME_STAMP:
343 state = getDateTimeTypeState(forecastData.getDt());
345 case CHANNEL_CONDITION:
346 if (!forecastData.getWeather().isEmpty()) {
347 state = getStringTypeState(forecastData.getWeather().get(0).getDescription());
350 case CHANNEL_CONDITION_ID:
351 if (!forecastData.getWeather().isEmpty()) {
352 state = getStringTypeState(forecastData.getWeather().get(0).getId().toString());
355 case CHANNEL_CONDITION_ICON:
356 if (!forecastData.getWeather().isEmpty()) {
357 state = getRawTypeState(
358 OpenWeatherMapConnection.getWeatherIcon(forecastData.getWeather().get(0).getIcon()));
361 case CHANNEL_CONDITION_ICON_ID:
362 if (!forecastData.getWeather().isEmpty()) {
363 state = getStringTypeState(forecastData.getWeather().get(0).getIcon());
366 case CHANNEL_TEMPERATURE:
367 state = getQuantityTypeState(forecastData.getMain().getTemp(), CELSIUS);
369 case CHANNEL_APPARENT_TEMPERATURE:
370 state = getQuantityTypeState(forecastData.getMain().getFeelsLikeTemp(), CELSIUS);
372 case CHANNEL_MIN_TEMPERATURE:
373 state = getQuantityTypeState(forecastData.getMain().getTempMin(), CELSIUS);
375 case CHANNEL_MAX_TEMPERATURE:
376 state = getQuantityTypeState(forecastData.getMain().getTempMax(), CELSIUS);
378 case CHANNEL_PRESSURE:
379 state = getQuantityTypeState(forecastData.getMain().getPressure(), HECTO(PASCAL));
381 case CHANNEL_HUMIDITY:
382 state = getQuantityTypeState(forecastData.getMain().getHumidity(), PERCENT);
384 case CHANNEL_WIND_SPEED:
385 state = getQuantityTypeState(forecastData.getWind().getSpeed(), METRE_PER_SECOND);
387 case CHANNEL_WIND_DIRECTION:
388 state = getQuantityTypeState(forecastData.getWind().getDeg(), DEGREE_ANGLE);
390 case CHANNEL_GUST_SPEED:
391 state = getQuantityTypeState(forecastData.getWind().getGust(), METRE_PER_SECOND);
393 case CHANNEL_CLOUDINESS:
394 state = getQuantityTypeState(forecastData.getClouds().getAll(), PERCENT);
397 Precipitation rain = forecastData.getRain();
398 state = getQuantityTypeState(rain == null ? 0 : rain.getVolume(), MILLI(METRE));
401 Precipitation snow = forecastData.getSnow();
402 state = getQuantityTypeState(snow == null ? 0 : snow.getVolume(), MILLI(METRE));
405 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
406 updateState(channelUID, state);
408 logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);
413 * Update the channel from the last OpenWeatherMap data retrieved.
415 * @param channelUID the id identifying the channel to be updated
418 private void updateDailyForecastChannel(ChannelUID channelUID, int count) {
419 String channelId = channelUID.getIdWithoutGroup();
420 String channelGroupId = channelUID.getGroupId();
421 OpenWeatherMapJsonDailyForecastData localDailyForecastData = dailyForecastData;
422 if (localDailyForecastData != null && localDailyForecastData.getList().size() > count) {
423 org.openhab.binding.openweathermap.internal.dto.forecast.daily.List forecastData = localDailyForecastData
424 .getList().get(count);
425 State state = UnDefType.UNDEF;
427 case CHANNEL_TIME_STAMP:
428 state = getDateTimeTypeState(forecastData.getDt());
430 case CHANNEL_SUNRISE:
431 state = getDateTimeTypeState(forecastData.getSunrise());
434 state = getDateTimeTypeState(forecastData.getSunset());
436 case CHANNEL_CONDITION:
437 if (!forecastData.getWeather().isEmpty()) {
438 state = getStringTypeState(forecastData.getWeather().get(0).getDescription());
441 case CHANNEL_CONDITION_ID:
442 if (!forecastData.getWeather().isEmpty()) {
443 state = getStringTypeState(forecastData.getWeather().get(0).getId().toString());
446 case CHANNEL_CONDITION_ICON:
447 if (!forecastData.getWeather().isEmpty()) {
448 state = getRawTypeState(
449 OpenWeatherMapConnection.getWeatherIcon(forecastData.getWeather().get(0).getIcon()));
452 case CHANNEL_CONDITION_ICON_ID:
453 if (!forecastData.getWeather().isEmpty()) {
454 state = getStringTypeState(forecastData.getWeather().get(0).getIcon());
457 case CHANNEL_MIN_TEMPERATURE:
458 state = getQuantityTypeState(forecastData.getTemp().getMin(), CELSIUS);
460 case CHANNEL_MAX_TEMPERATURE:
461 state = getQuantityTypeState(forecastData.getTemp().getMax(), CELSIUS);
463 case CHANNEL_APPARENT_TEMPERATURE:
464 FeelsLikeTemp feelsLikeTemp = forecastData.getFeelsLike();
465 if (feelsLikeTemp != null) {
466 state = getQuantityTypeState(feelsLikeTemp.getDay(), CELSIUS);
469 case CHANNEL_PRESSURE:
470 state = getQuantityTypeState(forecastData.getPressure(), HECTO(PASCAL));
472 case CHANNEL_HUMIDITY:
473 state = getQuantityTypeState(forecastData.getHumidity(), PERCENT);
475 case CHANNEL_WIND_SPEED:
476 state = getQuantityTypeState(forecastData.getSpeed(), METRE_PER_SECOND);
478 case CHANNEL_WIND_DIRECTION:
479 state = getQuantityTypeState(forecastData.getDeg(), DEGREE_ANGLE);
481 case CHANNEL_GUST_SPEED:
482 state = getQuantityTypeState(forecastData.getGust(), METRE_PER_SECOND);
484 case CHANNEL_CLOUDINESS:
485 state = getQuantityTypeState(forecastData.getClouds(), PERCENT);
488 Double rain = forecastData.getRain();
489 state = getQuantityTypeState(rain == null ? 0 : rain, MILLI(METRE));
492 Double snow = forecastData.getSnow();
493 state = getQuantityTypeState(snow == null ? 0 : snow, MILLI(METRE));
495 case CHANNEL_PRECIP_PROBABILITY:
496 Double probability = forecastData.getPop();
497 state = getQuantityTypeState(probability == null ? 0 : probability * 100.0, PERCENT);
500 logger.debug("Update channel '{}' of group '{}' with new state '{}'.", channelId, channelGroupId, state);
501 updateState(channelUID, state);
503 logger.debug("No weather data available to update channel '{}' of group '{}'.", channelId, channelGroupId);