diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java index bdcf0f52e48..4028c425e26 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/CommunicationManager.java @@ -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 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; diff --git a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java index 7a057506895..6c8f51b1ab0 100644 --- a/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java +++ b/bundles/org.openhab.binding.govee/src/main/java/org/openhab/binding/govee/internal/GoveeHandler.java @@ -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 index 00000000000..7f048867a11 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeHandlerMock.java @@ -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 index 00000000000..cb92a747806 --- /dev/null +++ b/bundles/org.openhab.binding.govee/src/test/java/org/openhab/binding/govee/internal/GoveeSerializeGoveeHandlerTest.java @@ -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 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(); + } + } +}