Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[miio] Fix parsing error clear record #17350

Merged
merged 6 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -504,19 +505,20 @@ 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();
switch (i) {
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());
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ThingStatusInfo> 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)));
}
}