From 1e007cd3050e6f27d34b607cca6a6439d899d74f Mon Sep 17 00:00:00 2001 From: lsiepel Date: Sun, 8 Sep 2024 21:26:59 +0200 Subject: [PATCH] [openweathermap] Fix `NullPointerException` (#17189) * Fix compilation warnings Signed-off-by: Leo Siepel --- .../OpenWeatherMapBindingConstants.java | 2 + .../OpenWeatherMapAPIConfiguration.java | 6 +- ...enWeatherMapAirPollutionConfiguration.java | 3 +- .../OpenWeatherMapLocationConfiguration.java | 4 +- .../OpenWeatherMapOneCallConfiguration.java | 4 +- ...herMapWeatherAndForecastConfiguration.java | 3 +- .../connection/OpenWeatherMapConnection.java | 8 +- .../dto/OpenWeatherMapOneCallHistAPIData.java | 7 +- .../internal/dto/base/Precipitation.java | 2 + .../AbstractOpenWeatherMapHandler.java | 10 +- .../OpenWeatherMapAirPollutionHandler.java | 7 +- .../handler/OpenWeatherMapOneCallHandler.java | 4 + .../OpenWeatherMapOneCallHistoryHandler.java | 17 +- ...enWeatherMapWeatherAndForecastHandler.java | 15 +- .../openweathermap/internal/DataUtil.java | 57 +++++ .../internal/TestObjectsUtil.java | 74 ++++++ ...enWeatherMapOneCallHistoryHandlerTest.java | 230 ++++++++++++++++++ .../src/test/resources/history_v2_5.json | 49 ++++ .../src/test/resources/history_v3_0.json | 31 +++ 19 files changed, 503 insertions(+), 30 deletions(-) create mode 100644 bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/DataUtil.java create mode 100644 bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/TestObjectsUtil.java create mode 100644 bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHistoryHandlerTest.java create mode 100644 bundles/org.openhab.binding.openweathermap/src/test/resources/history_v2_5.json create mode 100644 bundles/org.openhab.binding.openweathermap/src/test/resources/history_v3_0.json diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/OpenWeatherMapBindingConstants.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/OpenWeatherMapBindingConstants.java index d7c9edfe3d..b75f706a22 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/OpenWeatherMapBindingConstants.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/OpenWeatherMapBindingConstants.java @@ -45,6 +45,8 @@ public class OpenWeatherMapBindingConstants { public static final String CONFIG_API_KEY = "apikey"; public static final String CONFIG_LANGUAGE = "language"; public static final String CONFIG_LOCATION = "location"; + public static final String CONFIG_HISTORY_DAYS = "historyDay"; + public static final String CONFIG_API_VERSION = "apiVersion"; // Channel group types public static final ChannelGroupTypeUID CHANNEL_GROUP_TYPE_STATION = new ChannelGroupTypeUID(BINDING_ID, "station"); diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapAPIConfiguration.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapAPIConfiguration.java index 482394a0cf..137f2a339c 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapAPIConfiguration.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapAPIConfiguration.java @@ -16,10 +16,10 @@ import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.openweathermap.internal.handler.OpenWeatherMapAPIHandler; /** - * The {@link OpenWeatherMapAPIConfiguration} is the class used to match the {@link OpenWeatherMapAPIHandler}s + * The {@link OpenWeatherMapAPIConfiguration} is the class used to match the + * {@link org.openhab.binding.openweathermap.internal.handler.OpenWeatherMapAPIHandler}s * configuration. * * @author Christoph Weitkamp - Initial contribution @@ -32,7 +32,7 @@ public class OpenWeatherMapAPIConfiguration { "lt", "mk", "nl", "no", "pl", "pt", "pt_br", "ro", "ru", "se", "sv", "sk", "sl", "sr", "th", "tr", "uk", "ua", "vi", "zh_cn", "zh_tw", "zu"); - public @Nullable String apikey; + public String apikey = ""; public int refreshInterval; public @Nullable String language; public String apiVersion = "2.5"; diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapAirPollutionConfiguration.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapAirPollutionConfiguration.java index 185f22159e..211689240d 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapAirPollutionConfiguration.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapAirPollutionConfiguration.java @@ -13,11 +13,10 @@ package org.openhab.binding.openweathermap.internal.config; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.openweathermap.internal.handler.OpenWeatherMapAirPollutionHandler; /** * The {@link OpenWeatherMapAirPollutionConfiguration} is the class used to match the - * {@link OpenWeatherMapAirPollutionHandler}s configuration. + * {@link org.openhab.binding.openweathermap.internal.handler.OpenWeatherMapAirPollutionHandler}s configuration. * * @author Christoph Weitkamp - Initial contribution */ diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapLocationConfiguration.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapLocationConfiguration.java index 7c2b390961..905df656ca 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapLocationConfiguration.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapLocationConfiguration.java @@ -13,10 +13,10 @@ package org.openhab.binding.openweathermap.internal.config; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.openweathermap.internal.handler.AbstractOpenWeatherMapHandler; /** - * The {@link OpenWeatherMapLocationConfiguration} is the class used to match the {@link AbstractOpenWeatherMapHandler}s + * The {@link OpenWeatherMapLocationConfiguration} is the class used to match the + * {@link org.openhab.binding.openweathermap.internal.handler.AbstractOpenWeatherMapHandler}s * configuration. * * @author Christoph Weitkamp - Initial contribution diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapOneCallConfiguration.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapOneCallConfiguration.java index 5fc926f1e3..c7f775adb1 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapOneCallConfiguration.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapOneCallConfiguration.java @@ -13,10 +13,10 @@ package org.openhab.binding.openweathermap.internal.config; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.openweathermap.internal.handler.OpenWeatherMapOneCallHandler; /** - * The {@link OpenWeatherMapOneCallConfiguration} is the class used to match the {@link OpenWeatherMapOneCallHandler}s + * The {@link OpenWeatherMapOneCallConfiguration} is the class used to match the + * {@link org.openhab.binding.openweathermap.internal.handler.OpenWeatherMapOneCallHandler}s * configuration. * * @author Wolfgang Klimt - Initial contribution diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapWeatherAndForecastConfiguration.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapWeatherAndForecastConfiguration.java index 8a28fa63fb..59c0649191 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapWeatherAndForecastConfiguration.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapWeatherAndForecastConfiguration.java @@ -13,11 +13,10 @@ package org.openhab.binding.openweathermap.internal.config; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.openweathermap.internal.handler.OpenWeatherMapWeatherAndForecastHandler; /** * The {@link OpenWeatherMapWeatherAndForecastConfiguration} is the class used to match the - * {@link OpenWeatherMapWeatherAndForecastHandler}s configuration. + * {@link org.openhab.binding.openweathermap.internal.handler.OpenWeatherMapWeatherAndForecastHandler}s configuration. * * @author Christoph Weitkamp - Initial contribution */ diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/connection/OpenWeatherMapConnection.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/connection/OpenWeatherMapConnection.java index 8efc1e8d5a..aa05607de4 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/connection/OpenWeatherMapConnection.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/connection/OpenWeatherMapConnection.java @@ -314,11 +314,10 @@ public class OpenWeatherMapConnection { Map params = new HashMap<>(); // API key (see https://openweathermap.org/appid) - String apikey = config.apikey; - if (apikey == null || (apikey = apikey.trim()).isEmpty()) { + if (config.apikey.isBlank()) { throw new ConfigurationException("@text/offline.conf-error-missing-apikey"); } - params.put(PARAM_APPID, apikey); + params.put(PARAM_APPID, config.apikey); // Units format (see https://openweathermap.org/current#data) params.put(PARAM_UNITS, "metric"); @@ -380,6 +379,9 @@ public class OpenWeatherMapConnection { throw new ConfigurationException(errorMessage); case TOO_MANY_REQUESTS_429: // TODO disable refresh job temporarily (see https://openweathermap.org/appid#Accesslimitation) + errorMessage = getErrorMessage(content); + logger.debug("OpenWeatherMap server responded with status code {}: {}", httpStatus, errorMessage); + throw new CommunicationException(errorMessage); default: errorMessage = getErrorMessage(content); logger.debug("OpenWeatherMap server responded with status code {}: {}", httpStatus, errorMessage); diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/dto/OpenWeatherMapOneCallHistAPIData.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/dto/OpenWeatherMapOneCallHistAPIData.java index 6efafe12e4..a1c0b0b306 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/dto/OpenWeatherMapOneCallHistAPIData.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/dto/OpenWeatherMapOneCallHistAPIData.java @@ -32,6 +32,7 @@ public class OpenWeatherMapOneCallHistAPIData { private String timezone; @SerializedName("timezone_offset") private int timezoneOffset; + private Current[] data; private Current current; private List hourly = null; @@ -68,7 +69,11 @@ public class OpenWeatherMapOneCallHistAPIData { } public Current getCurrent() { - return current; + if (current != null) { + return current; + } else { + return data[0]; + } } public void setCurrent(Current current) { diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/dto/base/Precipitation.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/dto/base/Precipitation.java index ad0cb40bc6..72a842ab9a 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/dto/base/Precipitation.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/dto/base/Precipitation.java @@ -36,6 +36,8 @@ public class Precipitation { } public Double getVolume() { + Double oneHour = this.oneHour; + Double threeHours = this.threeHours; return oneHour != null ? oneHour : threeHours != null ? threeHours / 3 : 0; } } diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/AbstractOpenWeatherMapHandler.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/AbstractOpenWeatherMapHandler.java index 846ca2554b..e503de5c40 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/AbstractOpenWeatherMapHandler.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/AbstractOpenWeatherMapHandler.java @@ -83,11 +83,10 @@ public abstract class AbstractOpenWeatherMapHandler extends BaseThingHandler { public void initialize() { OpenWeatherMapLocationConfiguration config = getConfigAs(OpenWeatherMapLocationConfiguration.class); - boolean configValid = true; if (config.location == null || config.location.trim().isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/offline.conf-error-missing-location"); - configValid = false; + return; } try { @@ -96,13 +95,10 @@ public abstract class AbstractOpenWeatherMapHandler extends BaseThingHandler { logger.warn("Error parsing 'location' parameter: {}", e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/offline.conf-error-parsing-location"); - location = null; - configValid = false; + return; } - if (configValid) { - updateStatus(ThingStatus.UNKNOWN); - } + updateStatus(ThingStatus.UNKNOWN); } @Override diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapAirPollutionHandler.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapAirPollutionHandler.java index 41d530cb72..099a3478fc 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapAirPollutionHandler.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapAirPollutionHandler.java @@ -127,7 +127,12 @@ public class OpenWeatherMapAirPollutionHandler extends AbstractOpenWeatherMapHan @Override protected void updateChannel(ChannelUID channelUID) { - switch (channelUID.getGroupId()) { + String channelGroupId = channelUID.getGroupId(); + if (channelGroupId == null) { + logger.debug("Cannot update {} as it has no GroupId", channelUID); + return; + } + switch (channelGroupId) { case CHANNEL_GROUP_CURRENT_AIR_POLLUTION: updateCurrentAirPollutionChannel(channelUID); break; diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHandler.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHandler.java index daf763e7ee..3a702778e8 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHandler.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHandler.java @@ -239,6 +239,10 @@ public class OpenWeatherMapOneCallHandler extends AbstractOpenWeatherMapHandler @Override protected void updateChannel(ChannelUID channelUID) { String channelGroupId = channelUID.getGroupId(); + if (channelGroupId == null) { + logger.debug("Cannot update {} as it has no GroupId", channelUID); + return; + } logger.debug("OneCallHandler: updateChannel {}, groupID {}", channelUID, channelGroupId); switch (channelGroupId) { case CHANNEL_GROUP_ONECALL_CURRENT: diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHistoryHandler.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHistoryHandler.java index 5baf285b43..09dde6d42f 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHistoryHandler.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHistoryHandler.java @@ -17,6 +17,7 @@ import static org.openhab.core.library.unit.MetricPrefix.*; import static org.openhab.core.library.unit.SIUnits.*; import static org.openhab.core.library.unit.Units.*; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -103,6 +104,10 @@ public class OpenWeatherMapOneCallHistoryHandler extends AbstractOpenWeatherMapH @Override protected void updateChannel(ChannelUID channelUID) { String channelGroupId = channelUID.getGroupId(); + if (channelGroupId == null) { + logger.debug("Cannot update {} as it has no GroupId", channelUID); + return; + } switch (channelGroupId) { case CHANNEL_GROUP_ONECALL_HISTORY: updateHistoryCurrentChannel(channelUID); @@ -233,9 +238,17 @@ public class OpenWeatherMapOneCallHistoryHandler extends AbstractOpenWeatherMapH String channelGroupId = channelUID.getGroupId(); logger.debug("Updating hourly history data for channel {}, group {}, count {}", channelId, channelGroupId, count); + OpenWeatherMapOneCallHistAPIData localWeatherData = weatherData; - if (localWeatherData != null && localWeatherData.getHourly().size() > count) { - Hourly historyData = localWeatherData.getHourly().get(count); + List hourly; + if (localWeatherData == null || (hourly = localWeatherData.getHourly()) == null) { + logger.warn("No weather data available for channel {}, possible cause: api v3.0 does not support it", + channelUID); + return; + } + + if (hourly.size() > count) { + Hourly historyData = hourly.get(count); State state = UnDefType.UNDEF; switch (channelId) { case CHANNEL_TIME_STAMP: diff --git a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapWeatherAndForecastHandler.java b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapWeatherAndForecastHandler.java index bba5e46b01..61534d7e52 100644 --- a/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapWeatherAndForecastHandler.java +++ b/bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapWeatherAndForecastHandler.java @@ -179,11 +179,12 @@ public class OpenWeatherMapWeatherAndForecastHandler extends AbstractOpenWeather editConfig.put(CONFIG_FORECAST_DAYS, 0); updateConfiguration(editConfig); logger.debug("Removing daily forecast channel groups."); - List channels = getThing().getChannels().stream() - .filter(c -> CHANNEL_GROUP_FORECAST_TODAY.equals(c.getUID().getGroupId()) - || CHANNEL_GROUP_FORECAST_TOMORROW.equals(c.getUID().getGroupId()) - || c.getUID().getGroupId().startsWith(CHANNEL_GROUP_DAILY_FORECAST_PREFIX)) - .collect(Collectors.toList()); + List channels = getThing().getChannels().stream().filter(c -> { + String groupId = c.getUID().getGroupId(); + return CHANNEL_GROUP_FORECAST_TODAY.equals(groupId) + || CHANNEL_GROUP_FORECAST_TOMORROW.equals(groupId) + || (groupId != null && groupId.startsWith(CHANNEL_GROUP_DAILY_FORECAST_PREFIX)); + }).collect(Collectors.toList()); updateThing(editThing().withoutChannels(channels).build()); } else { throw e; @@ -200,6 +201,10 @@ public class OpenWeatherMapWeatherAndForecastHandler extends AbstractOpenWeather @Override protected void updateChannel(ChannelUID channelUID) { String channelGroupId = channelUID.getGroupId(); + if (channelGroupId == null) { + logger.debug("Cannot update {} as it has no GroupId", channelUID); + return; + } switch (channelGroupId) { case CHANNEL_GROUP_STATION: case CHANNEL_GROUP_CURRENT_WEATHER: diff --git a/bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/DataUtil.java b/bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/DataUtil.java new file mode 100644 index 0000000000..b374f36133 --- /dev/null +++ b/bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/DataUtil.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.openweathermap.internal; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.Gson; + +/** + * Utility class for working with test data in unit tests + * + * @author Leo Siepel - Initial contribution + */ +@NonNullByDefault +public class DataUtil { + @SuppressWarnings("null") + public static Reader openDataReader(String fileName) throws FileNotFoundException { + String filePath = "src/test/resources/" + fileName; + + InputStream inputStream = new FileInputStream(filePath); + return new InputStreamReader(inputStream, StandardCharsets.UTF_8); + } + + public static T fromJson(String fileName, Type typeOfT) throws IOException { + try (Reader reader = openDataReader(fileName)) { + return new Gson().fromJson(reader, typeOfT); + } + } + + @SuppressWarnings("null") + public static String fromFile(String fileName) throws IOException { + try (Reader reader = openDataReader(fileName)) { + return new BufferedReader(reader).lines().parallel().collect(Collectors.joining("\n")); + } + } +} diff --git a/bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/TestObjectsUtil.java b/bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/TestObjectsUtil.java new file mode 100644 index 0000000000..691cccc53d --- /dev/null +++ b/bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/TestObjectsUtil.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.openweathermap.internal; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.mockito.Mockito; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.type.ChannelKind; +import org.openhab.core.types.State; + +/** + * Utility class for working with test objects in unit tests + * + * @author Leo Siepel - Initial contribution + */ +@NonNullByDefault +public class TestObjectsUtil { + + public static Configuration createConfig(boolean returnValid, @Nullable String apiVersion) { + final Configuration config = new Configuration(); + if (returnValid) { + config.put(OpenWeatherMapBindingConstants.CONFIG_LOCATION, "51.0435,7.2865"); + config.put(OpenWeatherMapBindingConstants.CONFIG_HISTORY_DAYS, 1); + if (apiVersion != null) { + config.put(OpenWeatherMapBindingConstants.CONFIG_API_VERSION, apiVersion); + } + } + return config; + } + + public static Thing mockThing(Configuration configuration) { + final Thing thing = mock(Thing.class); + when(thing.getUID()).thenReturn(new ThingUID(OpenWeatherMapBindingConstants.BINDING_ID, "owm-test-thing")); + when(thing.getConfiguration()).thenReturn(configuration); + return thing; + } + + public static Channel mockChannel(final ThingUID thingId, final String channelId) { + final Channel channel = Mockito.mock(Channel.class); + when(channel.getUID()).thenReturn(new ChannelUID(thingId, channelId)); + when(channel.getKind()).thenReturn(ChannelKind.STATE); + + return channel; + } + + public static State getState(final double input, Unit unit) { + return new QuantityType<>(input, unit); + } + + public static State getState(final int input, Unit unit) { + return new QuantityType<>(input, unit); + } +} diff --git a/bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHistoryHandlerTest.java b/bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHistoryHandlerTest.java new file mode 100644 index 0000000000..b75d5a56f1 --- /dev/null +++ b/bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHistoryHandlerTest.java @@ -0,0 +1,230 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.openweathermap.internal.handler; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.openhab.binding.openweathermap.internal.OpenWeatherMapBindingConstants.*; +import static org.openhab.binding.openweathermap.internal.TestObjectsUtil.*; + +import java.io.IOException; +import java.math.BigDecimal; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.openweathermap.internal.DataUtil; +import org.openhab.binding.openweathermap.internal.TestObjectsUtil; +import org.openhab.binding.openweathermap.internal.connection.OpenWeatherMapConnection; +import org.openhab.binding.openweathermap.internal.dto.OpenWeatherMapOneCallHistAPIData; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.PointType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.types.State; + +/** + * @author Leo Siepel - Initial contribution + */ +@NonNullByDefault +public class OpenWeatherMapOneCallHistoryHandlerTest { + + private ThingHandlerCallback callback = mock(ThingHandlerCallback.class); + + private static Thing mockThing(Configuration configuration) { + final Thing thing = TestObjectsUtil.mockThing(configuration); + + final List channelList = Arrays.asList( + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_STATION_LOCATION), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_TIME_STAMP), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_SUNRISE), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_SUNSET), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_CONDITION), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_CONDITION_ID), // + // CHANNEL_CONDITION_ICON was left out of this test + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_CONDITION_ICON_ID), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_TEMPERATURE), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_APPARENT_TEMPERATURE), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_PRESSURE), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_HUMIDITY), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_DEW_POINT), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_WIND_SPEED), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_WIND_DIRECTION), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_GUST_SPEED), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_CLOUDINESS), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_UVINDEX), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_RAIN), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_SNOW), // + mockChannel(thing.getUID(), CHANNEL_GROUP_ONECALL_HISTORY + "#" + CHANNEL_VISIBILITY) // + ); + + when(thing.getChannels()).thenReturn(channelList); + return thing; + } + + private static OpenWeatherMapOneCallHistoryHandler createAndInitHandler(final ThingHandlerCallback callback, + final Thing thing) { + TimeZoneProvider timeZoneProvider = mock(TimeZoneProvider.class); + when(timeZoneProvider.getTimeZone()).thenReturn(ZoneId.of("UTC")); + final OpenWeatherMapOneCallHistoryHandler handler = spy( + new OpenWeatherMapOneCallHistoryHandler(thing, timeZoneProvider)); + + when(callback.isChannelLinked(any())).thenReturn(true); + + handler.setCallback(callback); + handler.initialize(); + + return handler; + } + + private static void assertGroupChannelStateSet(ThingHandlerCallback callback, ThingUID uid, String channel, + State state) { + verify(callback).stateUpdated(new ChannelUID(uid, CHANNEL_GROUP_ONECALL_HISTORY + "#" + channel), state); + } + + @Test + public void testInvalidConfiguration() { + // Arrange + final Configuration configuration = createConfig(false, null); + final Thing thing = mockThing(configuration); + final OpenWeatherMapOneCallHistoryHandler handler = createAndInitHandler(callback, thing); + + try { + verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.OFFLINE) + && arg.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR))); + } finally { + handler.dispose(); + } + } + + @Test + public void testCurrentWithResponseMessageV30() throws IOException { + // Arrange + final Configuration configuration = createConfig(true, "3.0"); + final Thing thing = mockThing(configuration); + final OpenWeatherMapOneCallHistoryHandler handler = createAndInitHandler(callback, thing); + + OpenWeatherMapOneCallHistAPIData data = DataUtil.fromJson("history_v3_0.json", + OpenWeatherMapOneCallHistAPIData.class); + OpenWeatherMapConnection connectionMock = mock(OpenWeatherMapConnection.class); + when(connectionMock.getOneCallHistAPIData(handler.location, + ((BigDecimal) configuration.get(CONFIG_HISTORY_DAYS)).intValue())).thenReturn(data); + + // Act + handler.updateData(connectionMock); + + // Assert + ThingUID uid = thing.getUID(); + try { + verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + verify(callback, atLeast(2)).statusUpdated(eq(thing), + argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE))); + + assertGroupChannelStateSet(callback, uid, CHANNEL_STATION_LOCATION, new PointType("52.2297,21.0122")); + assertGroupChannelStateSet(callback, uid, CHANNEL_TIME_STAMP, + new DateTimeType("2022-02-26T15:22:56.000+0000")); + assertGroupChannelStateSet(callback, uid, CHANNEL_SUNRISE, + new DateTimeType("2022-02-26T05:29:21.000+0000")); + assertGroupChannelStateSet(callback, uid, CHANNEL_SUNSET, new DateTimeType("2022-02-26T16:08:47.000+0000")); + assertGroupChannelStateSet(callback, uid, CHANNEL_CONDITION, new StringType("clear sky")); + assertGroupChannelStateSet(callback, uid, CHANNEL_CONDITION_ID, new StringType("800")); + // CHANNEL_CONDITION_ICON was left out of this test + assertGroupChannelStateSet(callback, uid, CHANNEL_CONDITION_ICON_ID, new StringType("01d")); + assertGroupChannelStateSet(callback, uid, CHANNEL_TEMPERATURE, getState(279.13, SIUnits.CELSIUS)); + assertGroupChannelStateSet(callback, uid, CHANNEL_APPARENT_TEMPERATURE, getState(276.44, SIUnits.CELSIUS)); + assertGroupChannelStateSet(callback, uid, CHANNEL_PRESSURE, getState(102900, SIUnits.PASCAL)); + assertGroupChannelStateSet(callback, uid, CHANNEL_HUMIDITY, getState(64, Units.PERCENT)); + assertGroupChannelStateSet(callback, uid, CHANNEL_DEW_POINT, getState(272.88, SIUnits.CELSIUS)); + assertGroupChannelStateSet(callback, uid, CHANNEL_WIND_SPEED, getState(3.6, Units.METRE_PER_SECOND)); + assertGroupChannelStateSet(callback, uid, CHANNEL_WIND_DIRECTION, getState(340, Units.DEGREE_ANGLE)); + assertGroupChannelStateSet(callback, uid, CHANNEL_GUST_SPEED, getState(0, Units.METRE_PER_SECOND)); + assertGroupChannelStateSet(callback, uid, CHANNEL_CLOUDINESS, getState(0, Units.PERCENT)); + assertGroupChannelStateSet(callback, uid, CHANNEL_UVINDEX, new DecimalType(0.06)); + assertGroupChannelStateSet(callback, uid, CHANNEL_RAIN, getState(0.0, SIUnits.METRE)); + assertGroupChannelStateSet(callback, uid, CHANNEL_SNOW, getState(0.0, SIUnits.METRE)); + assertGroupChannelStateSet(callback, uid, CHANNEL_VISIBILITY, getState(10000, SIUnits.METRE)); + } finally { + handler.dispose(); + } + } + + @Test + public void testCurrentWithResponseMessageV25() throws IOException { + // Arrange + final Configuration configuration = createConfig(true, "3.0"); + final Thing thing = mockThing(configuration); + final OpenWeatherMapOneCallHistoryHandler handler = createAndInitHandler(callback, thing); + + OpenWeatherMapOneCallHistAPIData data = DataUtil.fromJson("history_v2_5.json", + OpenWeatherMapOneCallHistAPIData.class); + OpenWeatherMapConnection connectionMock = mock(OpenWeatherMapConnection.class); + when(connectionMock.getOneCallHistAPIData(handler.location, + ((BigDecimal) configuration.get(CONFIG_HISTORY_DAYS)).intValue())).thenReturn(data); + + // Act + handler.updateData(connectionMock); + + // Assert + ThingUID uid = thing.getUID(); + try { + verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN))); + verify(callback, atLeast(2)).statusUpdated(eq(thing), + argThat(arg -> arg.getStatus().equals(ThingStatus.ONLINE))); + + assertGroupChannelStateSet(callback, uid, CHANNEL_STATION_LOCATION, new PointType("60.99,30.9")); + assertGroupChannelStateSet(callback, uid, CHANNEL_TIME_STAMP, + new DateTimeType("2020-04-09T21:33:47.000+0000")); + assertGroupChannelStateSet(callback, uid, CHANNEL_SUNRISE, + new DateTimeType("2020-04-10T02:57:04.000+0000")); + assertGroupChannelStateSet(callback, uid, CHANNEL_SUNSET, new DateTimeType("2020-04-10T17:04:57.000+0000")); + assertGroupChannelStateSet(callback, uid, CHANNEL_CONDITION, new StringType("clear sky")); + assertGroupChannelStateSet(callback, uid, CHANNEL_CONDITION_ID, new StringType("800")); + // CHANNEL_CONDITION_ICON was left out of this test + assertGroupChannelStateSet(callback, uid, CHANNEL_CONDITION_ICON_ID, new StringType("01n")); + assertGroupChannelStateSet(callback, uid, CHANNEL_TEMPERATURE, getState(274.31, SIUnits.CELSIUS)); + assertGroupChannelStateSet(callback, uid, CHANNEL_APPARENT_TEMPERATURE, getState(269.79, SIUnits.CELSIUS)); + assertGroupChannelStateSet(callback, uid, CHANNEL_PRESSURE, getState(100600, SIUnits.PASCAL)); + assertGroupChannelStateSet(callback, uid, CHANNEL_HUMIDITY, getState(72, Units.PERCENT)); + assertGroupChannelStateSet(callback, uid, CHANNEL_DEW_POINT, getState(270.21, SIUnits.CELSIUS)); + assertGroupChannelStateSet(callback, uid, CHANNEL_WIND_SPEED, getState(3, Units.METRE_PER_SECOND)); + assertGroupChannelStateSet(callback, uid, CHANNEL_WIND_DIRECTION, getState(260, Units.DEGREE_ANGLE)); + assertGroupChannelStateSet(callback, uid, CHANNEL_GUST_SPEED, getState(0, Units.METRE_PER_SECOND)); + assertGroupChannelStateSet(callback, uid, CHANNEL_CLOUDINESS, getState(0, Units.PERCENT)); + assertGroupChannelStateSet(callback, uid, CHANNEL_UVINDEX, new DecimalType(0.0)); + assertGroupChannelStateSet(callback, uid, CHANNEL_RAIN, getState(0.0, SIUnits.METRE)); + assertGroupChannelStateSet(callback, uid, CHANNEL_SNOW, getState(0.0, SIUnits.METRE)); + assertGroupChannelStateSet(callback, uid, CHANNEL_VISIBILITY, getState(10000, SIUnits.METRE)); + } finally { + handler.dispose(); + } + } +} diff --git a/bundles/org.openhab.binding.openweathermap/src/test/resources/history_v2_5.json b/bundles/org.openhab.binding.openweathermap/src/test/resources/history_v2_5.json new file mode 100644 index 0000000000..7c094874e5 --- /dev/null +++ b/bundles/org.openhab.binding.openweathermap/src/test/resources/history_v2_5.json @@ -0,0 +1,49 @@ +{ + "lat": 60.99, + "lon": 30.9, + "timezone": "Europe/Moscow", + "timezone_offset": 10800, + "current": { + "dt": 1586468027, + "sunrise": 1586487424, + "sunset": 1586538297, + "temp": 274.31, + "feels_like": 269.79, + "pressure": 1006, + "humidity": 72, + "dew_point": 270.21, + "clouds": 0, + "visibility": 10000, + "wind_speed": 3, + "wind_deg": 260, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ] + }, + "hourly": [ + { + "dt": 1586390400, + "temp": 278.41, + "feels_like": 269.43, + "pressure": 1006, + "humidity": 65, + "dew_point": 272.46, + "clouds": 0, + "wind_speed": 9.83, + "wind_deg": 60, + "wind_gust": 15.65, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01n" + } + ] + } ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.openweathermap/src/test/resources/history_v3_0.json b/bundles/org.openhab.binding.openweathermap/src/test/resources/history_v3_0.json new file mode 100644 index 0000000000..75757b39fc --- /dev/null +++ b/bundles/org.openhab.binding.openweathermap/src/test/resources/history_v3_0.json @@ -0,0 +1,31 @@ +{ + "lat": 52.2297, + "lon": 21.0122, + "timezone": "Europe/Warsaw", + "timezone_offset": 3600, + "data": [ + { + "dt": 1645888976, + "sunrise": 1645853361, + "sunset": 1645891727, + "temp": 279.13, + "feels_like": 276.44, + "pressure": 1029, + "humidity": 64, + "dew_point": 272.88, + "uvi": 0.06, + "clouds": 0, + "visibility": 10000, + "wind_speed": 3.6, + "wind_deg": 340, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ] + } + ] + } \ No newline at end of file -- 2.47.3