diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandler.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandler.java index de5da91fa50a9..2b0efc2e322ad 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandler.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandler.java @@ -98,6 +98,7 @@ public class MiIoVacuumHandler extends MiIoAbstractHandler { private final Logger logger = LoggerFactory.getLogger(MiIoVacuumHandler.class); private static final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); + private static final DateTimeFormatter PARSER_TZ = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); private static final Gson GSON = new GsonBuilder().serializeNulls().create(); private final ChannelUID mapChannelUid; @@ -504,6 +505,7 @@ private boolean updateHistory(JsonObject historyData) { private void updateHistoryRecordLegacy(JsonArray historyData) { HistoryRecordDTO historyRecord = new HistoryRecordDTO(); + for (int i = 0; i < historyData.size(); ++i) { try { BigInteger value = historyData.get(i).getAsBigInteger(); @@ -511,12 +513,12 @@ private void updateHistoryRecordLegacy(JsonArray historyData) { case 0: historyRecord.setStart(ZonedDateTime .ofInstant(Instant.ofEpochSecond(value.longValue()), ZoneId.systemDefault()) - .toString()); + .format(PARSER_TZ)); break; case 1: historyRecord.setEnd(ZonedDateTime .ofInstant(Instant.ofEpochSecond(value.longValue()), ZoneId.systemDefault()) - .toString()); + .format(PARSER_TZ)); break; case 2: historyRecord.setDuration(value.intValue()); @@ -549,14 +551,14 @@ private void updateHistoryRecordLegacy(JsonArray historyData) { private void updateHistoryRecord(HistoryRecordDTO historyRecordDTO) { JsonObject historyRecord = GSON.toJsonTree(historyRecordDTO).getAsJsonObject(); if (historyRecordDTO.getStart() != null) { - historyRecord.addProperty("start", historyRecordDTO.getStart().split("\\+")[0].split("\\-")[0]); - updateState(CHANNEL_HISTORY_START_TIME, - new DateTimeType(historyRecordDTO.getStart().split("\\+")[0].split("\\-")[0])); + DateTimeType start = new DateTimeType(historyRecordDTO.getStart()); + historyRecord.addProperty("start", start.toLocaleZone().format(null)); + updateState(CHANNEL_HISTORY_START_TIME, start); } if (historyRecordDTO.getEnd() != null) { - historyRecord.addProperty("end", historyRecordDTO.getEnd().split("\\+")[0].split("\\-")[0]); - updateState(CHANNEL_HISTORY_END_TIME, - new DateTimeType(historyRecordDTO.getEnd().split("\\+")[0].split("\\-")[0])); + DateTimeType end = new DateTimeType(historyRecordDTO.getEnd()); + historyRecord.addProperty("end", end.toLocaleZone().format(null)); + updateState(CHANNEL_HISTORY_END_TIME, end); } if (historyRecordDTO.getDuration() != null) { long duration = TimeUnit.SECONDS.toMinutes(historyRecordDTO.getDuration().longValue()); diff --git a/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandlerTest.java b/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandlerTest.java new file mode 100644 index 0000000000000..57f4144d461d0 --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/test/java/org/openhab/binding/miio/internal/handler/MiIoVacuumHandlerTest.java @@ -0,0 +1,171 @@ +/** + * 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.miio.internal.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.Timeout.ThreadMode; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.binding.miio.internal.MiIoBindingConstants; +import org.openhab.binding.miio.internal.MiIoCommand; +import org.openhab.binding.miio.internal.MiIoSendCommand; +import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService; +import org.openhab.binding.miio.internal.cloud.CloudConnector; +import org.openhab.binding.miio.internal.cloud.MiCloudException; +import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +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.ThingStatusInfo; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.type.ChannelTypeRegistry; + +import com.google.gson.JsonParser; + +/** + * Test case for {@link MiIoVacuumHandler} + * + * @author Marcel Verpaalen - Initial contribution + * + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class MiIoVacuumHandlerTest { + + private @NonNullByDefault({}) MiIoVacuumHandler miIoHandler; + private @Mock @NonNullByDefault({}) ThingHandlerCallback callback; + + private @Mock @NonNullByDefault({}) CloudConnector cloudConnector; + private @Mock @NonNullByDefault({}) MiIoDatabaseWatchService miIoDatabaseWatchService; + private @Mock @NonNullByDefault({}) ChannelTypeRegistry channelTypeRegistry; + private @Mock @NonNullByDefault({}) Thing thing; + private @Mock @NonNullByDefault({}) MiIoAsyncCommunication connection; + private @NonNullByDefault({}) @Mock TranslationProvider translationProvider; + private @NonNullByDefault({}) @Mock LocaleProvider localeProvider; + + private final Configuration configuration = new Configuration(); + private ThingUID thingUID = new ThingUID(MiIoBindingConstants.THING_TYPE_VACUUM, "TestThing"); + + @BeforeEach + public void setUp() throws IOException, MiCloudException { + configuration.put(MiIoBindingConstants.PROPERTY_HOST_IP, "localhost"); + configuration.put(MiIoBindingConstants.PROPERTY_TOKEN, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + configuration.put(MiIoBindingConstants.PROPERTY_DID, "AABBCCDDEEFF"); + configuration.put(MiIoBindingConstants.PROPERTY_CLOUDSERVER, "fake"); + configuration.put("communication", "cloud"); + + when(thing.getConfiguration()).thenReturn(configuration); + when(thing.getUID()).thenReturn(thingUID); + when(thing.getThingTypeUID()).thenReturn(MiIoBindingConstants.THING_TYPE_VACUUM); + when(cloudConnector.sendRPCCommand(any(), any(), any())).thenReturn("{\"result\":\"triggerError\"}"); + lenient().when(callback.isChannelLinked(any())).thenReturn(true); + + miIoHandler = new MiIoVacuumHandler(thing, miIoDatabaseWatchService, cloudConnector, channelTypeRegistry, + translationProvider, localeProvider); + + miIoHandler.setCallback(callback); + } + + @AfterEach + public void after() { + miIoHandler.dispose(); + } + + @Test + public void testInitializeShouldCallTheCallback() throws InterruptedException { + miIoHandler.initialize(); + ArgumentCaptor statusInfoCaptor = ArgumentCaptor.forClass(ThingStatusInfo.class); + verify(callback).statusUpdated(eq(thing), statusInfoCaptor.capture()); + ThingStatusInfo thingStatusInfo = statusInfoCaptor.getValue(); + assertEquals(ThingStatus.OFFLINE, thingStatusInfo.getStatus(), "Device should be OFFLINE"); + } + + @Test + @Timeout(value = 30, unit = TimeUnit.SECONDS, threadMode = ThreadMode.SEPARATE_THREAD) + public void TestCleanRecord() { + miIoHandler.initialize(); + // prepare a CLEAN_RECORD_GET response object + String cmdString = "{\"id\":7028,\"method\":\"get_clean_record\",\"params\":[1699081963]}"; + String jsonResponseTxt = "{\"result\":[[1724174413,1724174459,246,770000,0,0,2,3,60]],\"id\":7028}"; + + MiIoSendCommand response = new MiIoSendCommand(13, MiIoCommand.CLEAN_RECORD_GET, + JsonParser.parseString(cmdString).getAsJsonObject(), "", ""); + response.setResponse(JsonParser.parseString(jsonResponseTxt).getAsJsonObject()); + miIoHandler.onMessageReceived(response); + + verify(callback, description("Test the start time parsing")).stateUpdated( + eq(new ChannelUID(thingUID, MiIoBindingConstants.CHANNEL_HISTORY_START_TIME)), + eq(new DateTimeType(ZonedDateTime.parse("2024-08-20T19:20:13+02:00")).toZone(ZoneId.systemDefault()))); + + verify(callback, description("Test the end time parsing")).stateUpdated( + eq(new ChannelUID(thingUID, MiIoBindingConstants.CHANNEL_HISTORY_END_TIME)), + eq(new DateTimeType(ZonedDateTime.parse("2024-08-20T19:20:59+02:00")).toZone(ZoneId.systemDefault()))); + + verify(callback, description("Test the duration parsing")).stateUpdated( + eq(new ChannelUID(thingUID, MiIoBindingConstants.CHANNEL_HISTORY_DURATION)), + eq(new QuantityType<>(4, Units.MINUTE))); + } + + @Test + @Timeout(value = 30, unit = TimeUnit.SECONDS, threadMode = ThreadMode.SEPARATE_THREAD) + public void TestCleanSummary() { + miIoHandler.initialize(); + + ThingStatusInfo ts = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, "I fake to be online"); + when(thing.getStatusInfo()).thenReturn(ts); + + // prepare a CLEAN_SUMMARY_GET response object + String cmdString = "{\"id\":114,\"method\":\"get_clean_summary\",\"params\":[]}"; + String jsonResponseTxt = "{\"id\":114,\"result\":{\"clean_time\":109968,\"clean_area\":1694875000,\"clean_count\":51,\"dust_collection_count\":48,\"records\":[1699081963,1698999875,1698126572,1697463736,1697031817,1696486642,1696320557,1696253060,1695833343,1695821201,1695619374,1695476013,1695457865,1695274110,1695014622,1694876238,1694860994,1694755927,1694526730,1694237806]}}"; + + MiIoSendCommand response = new MiIoSendCommand(13, MiIoCommand.CLEAN_SUMMARY_GET, + JsonParser.parseString(cmdString).getAsJsonObject(), "", ""); + response.setResponse(JsonParser.parseString(jsonResponseTxt).getAsJsonObject()); + miIoHandler.onMessageReceived(response); + + verify(callback, description("Test clean time")).stateUpdated( + eq(new ChannelUID(thingUID, MiIoBindingConstants.CHANNEL_HISTORY_TOTALTIME)), + eq(new QuantityType<>(TimeUnit.MINUTES.convert(109968, TimeUnit.SECONDS), Units.MINUTE))); + + verify(callback, description("Test the area parsing")).stateUpdated( + eq(new ChannelUID(thingUID, MiIoBindingConstants.CHANNEL_HISTORY_TOTALAREA)), + eq(new QuantityType<>(1694.875, SIUnits.SQUARE_METRE))); + } +}