]> git.basschouten.com Git - openhab-addons.git/commitdiff
[openweathermap] Fix `NullPointerException` (#17189)
authorlsiepel <leosiepel@gmail.com>
Sun, 8 Sep 2024 19:26:59 +0000 (21:26 +0200)
committerGitHub <noreply@github.com>
Sun, 8 Sep 2024 19:26:59 +0000 (21:26 +0200)
* Fix compilation warnings

Signed-off-by: Leo Siepel <leosiepel@gmail.com>
19 files changed:
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/OpenWeatherMapBindingConstants.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapAPIConfiguration.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapAirPollutionConfiguration.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapLocationConfiguration.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapOneCallConfiguration.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/config/OpenWeatherMapWeatherAndForecastConfiguration.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/connection/OpenWeatherMapConnection.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/dto/OpenWeatherMapOneCallHistAPIData.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/dto/base/Precipitation.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/AbstractOpenWeatherMapHandler.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapAirPollutionHandler.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHandler.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHistoryHandler.java
bundles/org.openhab.binding.openweathermap/src/main/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapWeatherAndForecastHandler.java
bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/DataUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/TestObjectsUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.openweathermap/src/test/java/org/openhab/binding/openweathermap/internal/handler/OpenWeatherMapOneCallHistoryHandlerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.openweathermap/src/test/resources/history_v2_5.json [new file with mode: 0644]
bundles/org.openhab.binding.openweathermap/src/test/resources/history_v3_0.json [new file with mode: 0644]

index d7c9edfe3d7d56187a41f6b36771dc89cb9f89df..b75f706a22913a1c4ce5fa86a463ebc89413afa6 100644 (file)
@@ -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");
index 482394a0cf0996c8c2630ee94bd6f93b6ad323ee..137f2a339c9b91b5f83a127bb70550551157b62e 100644 (file)
@@ -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";
index 185f22159ea4ab7ec786795457469401b4acad7c..211689240d6c40574fe3f76b576d5cce50f32115 100644 (file)
 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
  */
index 7c2b3909617646b3a33d1210fb1ecd4aa57db978..905df656caf4ebedd547f0138d6de7ea6e387f95 100644 (file)
 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
index 5fc926f1e3a318c65ac527fbd6c57e31b2f18113..c7f775adb1f990f8ecf6dd61281908325b252416 100644 (file)
 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
index 8a28fa63fbab89409f96fcad7263aa4a0514769b..59c064919160ff94fba0cb1a9e17f2433c0be7eb 100644 (file)
 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
  */
index 8efc1e8d5a47eca5030788b4fbec2212dbc610ae..aa05607de4321467ee159ad87e300168e5f035c5 100644 (file)
@@ -314,11 +314,10 @@ public class OpenWeatherMapConnection {
 
         Map<String, String> 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);
index 6efafe12e499739855e74c027004fad210943861..a1c0b0b3067e2d1c1bda99ae327cedeadfaf6b90 100644 (file)
@@ -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> 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) {
index ad0cb40bc6437c5f0a7db9c13a549ce308f29512..72a842ab9abf21e4acd04265f01d98c0bb644de4 100644 (file)
@@ -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;
     }
 }
index 846ca2554b6f278c216ae331822b5e18b09c3a15..e503de5c406ec75a4dfcc1110e5c7a569b56dc85 100644 (file)
@@ -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
index 41d530cb72d087254b7c9e7451c157184337975d..099a3478fcdf69d17a6a785e5aa2cafe9dfe0ab8 100644 (file)
@@ -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;
index daf763e7ee1aa180ff70d69ca239dd95ac2d9208..3a702778e8b735a17f930adfea9a757ab5e2a830 100644 (file)
@@ -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:
index 5baf285b43724aa6aa09c259749278e29d9d3c2c..09dde6d42f05f537a5bb69e6aa809c1896dd4e91 100644 (file)
@@ -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> 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:
index bba5e46b0177a303db864700ce83040e56bbaa56..61534d7e5233591fc173ae7b1d411a6751944cd3 100644 (file)
@@ -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<Channel> 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<Channel> 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 (file)
index 0000000..b374f36
--- /dev/null
@@ -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> 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 (file)
index 0000000..691cccc
--- /dev/null
@@ -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 (file)
index 0000000..b75d5a5
--- /dev/null
@@ -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<Channel> 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 (file)
index 0000000..7c09487
--- /dev/null
@@ -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 (file)
index 0000000..75757b3
--- /dev/null
@@ -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