]> git.basschouten.com Git - openhab-addons.git/commitdiff
[govee] Fix invalid status response handling (#17048)
authorlsiepel <leosiepel@gmail.com>
Tue, 16 Jul 2024 17:48:17 +0000 (19:48 +0200)
committerGitHub <noreply@github.com>
Tue, 16 Jul 2024 17:48:17 +0000 (19:48 +0200)
Signed-off-by: Leo Siepel <leosiepel@gmail.com>
bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java
bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java
bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeHandlerMock.java [new file with mode: 0644]
bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeGoveeHandlerTest.java [new file with mode: 0644]

index bdcf0f52e482260a74d4cf822781a90f2c344e11..4028c425e26ee88d31164b2a88e2c3c1b6dde728 100644 (file)
@@ -47,6 +47,7 @@ import com.google.gson.JsonParseException;
 @NonNullByDefault
 @Component(service = CommunicationManager.class)
 public class CommunicationManager {
+    private final Logger logger = LoggerFactory.getLogger(CommunicationManager.class);
     private final Gson gson = new Gson();
     // Holds a list of all thing handlers to send them thing updates via the receiver-Thread
     private final Map<String, GoveeHandler> thingHandlers = new HashMap<>();
@@ -101,7 +102,7 @@ public class CommunicationManager {
         final byte[] data = message.getBytes();
         final InetAddress address = InetAddress.getByName(hostname);
         DatagramPacket packet = new DatagramPacket(data, data.length, address, REQUEST_PORT);
-        // logger.debug("Sending {} to {}", message, hostname);
+        logger.trace("Sending {} to {}", message, hostname);
         socket.send(packet);
         socket.close();
     }
@@ -224,7 +225,9 @@ public class CommunicationManager {
                                     }
                                 }
                             } catch (JsonParseException e) {
-                                // this probably was a status message
+                                logger.debug(
+                                        "JsonParseException when trying to parse the response, probably a status message",
+                                        e);
                             }
                         } else {
                             final @Nullable GoveeHandler handler;
index 7a057506895e3b0bc32e29b3226da44447743e90..6c8f51b1ab069547fb9009dda83135d6bc9e4de9 100644 (file)
@@ -15,6 +15,7 @@ package org.openhab.binding.govee.internal;
 import static org.openhab.binding.govee.internal.GoveeBindingConstants.*;
 
 import java.io.IOException;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
@@ -80,7 +81,7 @@ public class GoveeHandler extends BaseThingHandler {
     private static final Gson GSON = new Gson();
 
     private final Logger logger = LoggerFactory.getLogger(GoveeHandler.class);
-
+    protected ScheduledExecutorService executorService = scheduler;
     @Nullable
     private ScheduledFuture<?> triggerStatusJob; // send device status update job
     private GoveeConfiguration goveeConfiguration = new GoveeConfiguration();
@@ -100,9 +101,7 @@ public class GoveeHandler extends BaseThingHandler {
     private final Runnable thingRefreshSender = () -> {
         try {
             triggerDeviceStatusRefresh();
-            if (!thing.getStatus().equals(ThingStatus.ONLINE)) {
-                updateStatus(ThingStatus.ONLINE);
-            }
+            updateStatus(ThingStatus.ONLINE);
         } catch (IOException e) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                     "@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname
@@ -134,7 +133,7 @@ public class GoveeHandler extends BaseThingHandler {
         if (triggerStatusJob == null) {
             logger.debug("Starting refresh trigger job for thing {} ", thing.getLabel());
 
-            triggerStatusJob = scheduler.scheduleWithFixedDelay(thingRefreshSender, 100,
+            triggerStatusJob = executorService.scheduleWithFixedDelay(thingRefreshSender, 100,
                     goveeConfiguration.refreshInterval * 1000L, TimeUnit.MILLISECONDS);
         }
     }
@@ -195,9 +194,7 @@ public class GoveeHandler extends BaseThingHandler {
                         break;
                 }
             }
-            if (!thing.getStatus().equals(ThingStatus.ONLINE)) {
-                updateStatus(ThingStatus.ONLINE);
-            }
+            updateStatus(ThingStatus.ONLINE);
         } catch (IOException e) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                     "@text/offline.communication-error.could-not-query-device [\"" + goveeConfiguration.hostname
@@ -300,6 +297,10 @@ public class GoveeHandler extends BaseThingHandler {
         newColorTempInKelvin = (newColorTempInKelvin < COLOR_TEMPERATURE_MIN_VALUE)
                 ? COLOR_TEMPERATURE_MIN_VALUE.intValue()
                 : newColorTempInKelvin;
+        newColorTempInKelvin = (newColorTempInKelvin > COLOR_TEMPERATURE_MAX_VALUE)
+                ? COLOR_TEMPERATURE_MAX_VALUE.intValue()
+                : newColorTempInKelvin;
+
         int newColorTempInPercent = ((Double) ((newColorTempInKelvin - COLOR_TEMPERATURE_MIN_VALUE)
                 / (COLOR_TEMPERATURE_MAX_VALUE - COLOR_TEMPERATURE_MIN_VALUE) * 100.0)).intValue();
 
diff --git a/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeHandlerMock.java b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeHandlerMock.java
new file mode 100644 (file)
index 0000000..7f04886
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * 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.govee.internal;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.openhab.core.thing.Thing;
+
+/**
+ * The {@link GoveeHandlerMock} is responsible for mocking {@link GoveeHandler}
+ * 
+ * @author Leo Siepel - Initial contribution
+ */
+@NonNullByDefault
+public class GoveeHandlerMock extends GoveeHandler {
+
+    public GoveeHandlerMock(Thing thing, CommunicationManager communicationManager) {
+        super(thing, communicationManager);
+
+        executorService = Mockito.mock(ScheduledExecutorService.class);
+        doAnswer((InvocationOnMock invocation) -> {
+            ((Runnable) invocation.getArguments()[0]).run();
+            return null;
+        }).when(executorService).scheduleWithFixedDelay(any(Runnable.class), anyLong(), anyLong(), any(TimeUnit.class));
+    }
+}
diff --git a/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeGoveeHandlerTest.java b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeGoveeHandlerTest.java
new file mode 100644 (file)
index 0000000..cb92a74
--- /dev/null
@@ -0,0 +1,143 @@
+/**
+ * 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.govee.internal;
+
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+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 java.util.Arrays;
+import java.util.List;
+
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.openhab.binding.govee.internal.model.StatusResponse;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.HSBType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.QuantityType;
+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;
+
+import com.google.gson.Gson;
+
+/**
+ * @author Leo Siepel - Initial contribution
+ */
+@NonNullByDefault
+public class GoveeSerializeGoveeHandlerTest {
+
+    private static final Gson GSON = new Gson();
+    private final String invalidValueJsonString = "{\"msg\": {\"cmd\": \"devStatus\", \"data\": {\"onOff\": 0, \"brightness\": 100, \"color\": {\"r\": 1, \"g\": 10, \"b\": 0}, \"colorTemInKelvin\": 9070}}}";
+
+    private static final Configuration CONFIG = createConfig(true);
+    private static final Configuration BAD_CONFIG = createConfig(false);
+
+    private static Configuration createConfig(boolean returnValid) {
+        final Configuration config = new Configuration();
+        if (returnValid) {
+            config.put("hostname", "1.2.3.4");
+        }
+        return config;
+    }
+
+    private static Thing mockThing(boolean withConfiguration) {
+        final Thing thing = mock(Thing.class);
+        when(thing.getUID()).thenReturn(new ThingUID(GoveeBindingConstants.THING_TYPE_LIGHT, "govee-test-thing"));
+        when(thing.getConfiguration()).thenReturn(withConfiguration ? CONFIG : BAD_CONFIG);
+
+        final List<Channel> channelList = Arrays.asList(
+                mockChannel(thing.getUID(), GoveeBindingConstants.CHANNEL_COLOR), //
+                mockChannel(thing.getUID(), GoveeBindingConstants.CHANNEL_COLOR_TEMPERATURE), //
+                mockChannel(thing.getUID(), GoveeBindingConstants.CHANNEL_COLOR_TEMPERATURE_ABS));
+
+        when(thing.getChannels()).thenReturn(channelList);
+        return thing;
+    }
+
+    private static Channel mockChannel(final ThingUID thingId, final String channelId) {
+        final Channel channel = Mockito.mock(Channel.class);
+        when(channel.getUID()).thenReturn(new ChannelUID(thingId, channelId));
+        return channel;
+    }
+
+    private static GoveeHandlerMock createAndInitHandler(final ThingHandlerCallback callback, final Thing thing) {
+        CommunicationManager communicationManager = mock(CommunicationManager.class);
+        final GoveeHandlerMock handler = spy(new GoveeHandlerMock(thing, communicationManager));
+
+        handler.setCallback(callback);
+        handler.initialize();
+
+        return handler;
+    }
+
+    private static State getState(final int input, Unit<?> unit) {
+        return new QuantityType<>(input, unit);
+    }
+
+    @Test
+    public void testInvalidConfiguration() {
+        final Thing thing = mockThing(false);
+        final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
+        final GoveeHandlerMock 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 testInvalidResponseMessage() {
+        final Thing thing = mockThing(true);
+        final ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
+        final GoveeHandlerMock handler = createAndInitHandler(callback, thing);
+
+        // inject StatusResponseMessage
+        StatusResponse statusMessage = GSON.fromJson(invalidValueJsonString, StatusResponse.class);
+        handler.updateDeviceState(statusMessage);
+
+        try {
+            verify(callback).statusUpdated(eq(thing), argThat(arg -> arg.getStatus().equals(ThingStatus.UNKNOWN)));
+
+            verify(callback).stateUpdated(new ChannelUID(thing.getUID(), GoveeBindingConstants.CHANNEL_COLOR),
+                    new HSBType(new DecimalType(114), new PercentType(100), new PercentType(0)));
+
+            verify(callback).stateUpdated(
+                    new ChannelUID(thing.getUID(), GoveeBindingConstants.CHANNEL_COLOR_TEMPERATURE),
+                    new PercentType(100));
+
+            verify(callback).stateUpdated(
+                    new ChannelUID(thing.getUID(), GoveeBindingConstants.CHANNEL_COLOR_TEMPERATURE_ABS),
+                    getState(2000, Units.KELVIN));
+        } finally {
+            handler.dispose();
+        }
+    }
+}