[mielecloud] Fix washing machine can be started channel is not updated (#12583)
* Add tests to ensure that parsing works correctly * Fetch /actions on server sent event * Refactor onServerSentEvent * Remove ActionStateFetcher * Manually construct BigDecimal Signed-off-by: Björn Lange <bjoern.lange@itemis.de>
This commit is contained in:
parent
cda38435e9
commit
6b8db4fe0d
|
@ -27,7 +27,6 @@ import org.openhab.binding.mielecloud.internal.discovery.ThingInformationExtract
|
||||||
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
|
import org.openhab.binding.mielecloud.internal.handler.channel.ActionsChannelState;
|
||||||
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
|
import org.openhab.binding.mielecloud.internal.handler.channel.DeviceChannelState;
|
||||||
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
|
import org.openhab.binding.mielecloud.internal.handler.channel.TransitionChannelState;
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.ActionStateFetcher;
|
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
|
import org.openhab.binding.mielecloud.internal.webservice.MieleWebservice;
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.UnavailableMieleWebservice;
|
import org.openhab.binding.mielecloud.internal.webservice.UnavailableMieleWebservice;
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
|
import org.openhab.binding.mielecloud.internal.webservice.api.ActionsState;
|
||||||
|
@ -59,7 +58,6 @@ import org.slf4j.LoggerFactory;
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public abstract class AbstractMieleThingHandler extends BaseThingHandler {
|
public abstract class AbstractMieleThingHandler extends BaseThingHandler {
|
||||||
protected final ActionStateFetcher actionFetcher;
|
|
||||||
protected DeviceState latestDeviceState = new DeviceState(getDeviceId(), null);
|
protected DeviceState latestDeviceState = new DeviceState(getDeviceId(), null);
|
||||||
protected TransitionState latestTransitionState = new TransitionState(null, latestDeviceState);
|
protected TransitionState latestTransitionState = new TransitionState(null, latestDeviceState);
|
||||||
protected ActionsState latestActionsState = new ActionsState(getDeviceId(), null);
|
protected ActionsState latestActionsState = new ActionsState(getDeviceId(), null);
|
||||||
|
@ -73,7 +71,6 @@ public abstract class AbstractMieleThingHandler extends BaseThingHandler {
|
||||||
*/
|
*/
|
||||||
public AbstractMieleThingHandler(Thing thing) {
|
public AbstractMieleThingHandler(Thing thing) {
|
||||||
super(thing);
|
super(thing);
|
||||||
this.actionFetcher = new ActionStateFetcher(this::getWebservice, scheduler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<MieleBridgeHandler> getMieleBridgeHandler() {
|
private Optional<MieleBridgeHandler> getMieleBridgeHandler() {
|
||||||
|
@ -170,8 +167,6 @@ public abstract class AbstractMieleThingHandler extends BaseThingHandler {
|
||||||
* Invoked when a device state update for the device managed by this handler is received from the Miele cloud.
|
* Invoked when a device state update for the device managed by this handler is received from the Miele cloud.
|
||||||
*/
|
*/
|
||||||
public final void onDeviceStateUpdated(DeviceState deviceState) {
|
public final void onDeviceStateUpdated(DeviceState deviceState) {
|
||||||
actionFetcher.onDeviceStateUpdated(deviceState);
|
|
||||||
|
|
||||||
latestTransitionState = new TransitionState(latestTransitionState, deviceState);
|
latestTransitionState = new TransitionState(latestTransitionState, deviceState);
|
||||||
latestDeviceState = deviceState;
|
latestDeviceState = deviceState;
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
*/
|
*/
|
||||||
package org.openhab.binding.mielecloud.internal.handler.channel;
|
package org.openhab.binding.mielecloud.internal.handler.channel;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
@ -53,14 +54,14 @@ public final class ChannelTypeUtil {
|
||||||
* Converts an {@link Optional} of {@link Integer} to {@link State}.
|
* Converts an {@link Optional} of {@link Integer} to {@link State}.
|
||||||
*/
|
*/
|
||||||
public static State intToState(Optional<Integer> value) {
|
public static State intToState(Optional<Integer> value) {
|
||||||
return value.map(v -> (State) new DecimalType(v)).orElse(UnDefType.UNDEF);
|
return value.map(v -> (State) new DecimalType(new BigDecimal(v))).orElse(UnDefType.UNDEF);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an {@link Optional} of {@link Long} to {@link State}.
|
* Converts an {@link Optional} of {@link Long} to {@link State}.
|
||||||
*/
|
*/
|
||||||
public static State longToState(Optional<Long> value) {
|
public static State longToState(Optional<Long> value) {
|
||||||
return value.map(v -> (State) new DecimalType(v)).orElse(UnDefType.UNDEF);
|
return value.map(v -> (State) new DecimalType(new BigDecimal(v))).orElse(UnDefType.UNDEF);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2010-2022 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.mielecloud.internal.webservice;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
|
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
|
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
|
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link ActionStateFetcher} fetches the updated actions state for a device from the {@link MieleWebservice} if
|
|
||||||
* the state of that device changed.
|
|
||||||
*
|
|
||||||
* Note that an instance of this class is required for each device.
|
|
||||||
*
|
|
||||||
* @author Roland Edelhoff - Initial contribution
|
|
||||||
* @author Björn Lange - Make calls to webservice asynchronous
|
|
||||||
*/
|
|
||||||
@NonNullByDefault
|
|
||||||
public class ActionStateFetcher {
|
|
||||||
private Optional<DeviceState> lastDeviceState = Optional.empty();
|
|
||||||
private final Supplier<MieleWebservice> webserviceSupplier;
|
|
||||||
private final ScheduledExecutorService scheduler;
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(ActionStateFetcher.class);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new {@link ActionStateFetcher}.
|
|
||||||
*
|
|
||||||
* @param webserviceSupplier Getter function for access to the {@link MieleWebservice}.
|
|
||||||
* @param scheduler System-wide scheduler.
|
|
||||||
*/
|
|
||||||
public ActionStateFetcher(Supplier<MieleWebservice> webserviceSupplier, ScheduledExecutorService scheduler) {
|
|
||||||
this.webserviceSupplier = webserviceSupplier;
|
|
||||||
this.scheduler = scheduler;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invoked when the state of a device was updated.
|
|
||||||
*/
|
|
||||||
public void onDeviceStateUpdated(DeviceState deviceState) {
|
|
||||||
if (hasDeviceStatusChanged(deviceState)) {
|
|
||||||
scheduler.submit(() -> fetchActions(deviceState));
|
|
||||||
}
|
|
||||||
lastDeviceState = Optional.of(deviceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasDeviceStatusChanged(DeviceState newDeviceState) {
|
|
||||||
return lastDeviceState.map(DeviceState::getStateType)
|
|
||||||
.map(rawStatus -> !newDeviceState.getStateType().equals(rawStatus)).orElse(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchActions(DeviceState deviceState) {
|
|
||||||
try {
|
|
||||||
webserviceSupplier.get().fetchActions(deviceState.getDeviceIdentifier());
|
|
||||||
} catch (MieleWebserviceException e) {
|
|
||||||
logger.warn("Failed to fetch action state for device {}: {} - {}", deviceState.getDeviceIdentifier(),
|
|
||||||
e.getConnectionError(), e.getMessage());
|
|
||||||
} catch (AuthorizationFailedException | TooManyRequestsException e) {
|
|
||||||
logger.warn("Failed to fetch action state for device {}: {}", deviceState.getDeviceIdentifier(),
|
|
||||||
e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,6 +25,7 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.eclipse.jetty.client.api.ContentResponse;
|
import org.eclipse.jetty.client.api.ContentResponse;
|
||||||
import org.eclipse.jetty.client.api.Request;
|
import org.eclipse.jetty.client.api.Request;
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
|
import org.openhab.binding.mielecloud.internal.webservice.api.json.Actions;
|
||||||
|
import org.openhab.binding.mielecloud.internal.webservice.api.json.ActionsCollection;
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
|
import org.openhab.binding.mielecloud.internal.webservice.api.json.DeviceCollection;
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
|
import org.openhab.binding.mielecloud.internal.webservice.api.json.Light;
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.api.json.MieleSyntaxException;
|
import org.openhab.binding.mielecloud.internal.webservice.api.json.MieleSyntaxException;
|
||||||
|
@ -64,6 +65,7 @@ public final class DefaultMieleWebservice implements MieleWebservice, SseListene
|
||||||
private static final String ENDPOINT_ALL_SSE_EVENTS = ENDPOINT_DEVICES + "all/events";
|
private static final String ENDPOINT_ALL_SSE_EVENTS = ENDPOINT_DEVICES + "all/events";
|
||||||
|
|
||||||
private static final String SSE_EVENT_TYPE_DEVICES = "devices";
|
private static final String SSE_EVENT_TYPE_DEVICES = "devices";
|
||||||
|
public static final String SSE_EVENT_TYPE_ACTIONS = "actions";
|
||||||
|
|
||||||
private static final Gson GSON = new Gson();
|
private static final Gson GSON = new Gson();
|
||||||
|
|
||||||
|
@ -142,12 +144,37 @@ public final class DefaultMieleWebservice implements MieleWebservice, SseListene
|
||||||
public void onServerSentEvent(ServerSentEvent event) {
|
public void onServerSentEvent(ServerSentEvent event) {
|
||||||
fireConnectionAlive();
|
fireConnectionAlive();
|
||||||
|
|
||||||
if (!SSE_EVENT_TYPE_DEVICES.equals(event.getEvent())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deviceStateDispatcher.dispatchDeviceStateUpdates(DeviceCollection.fromJson(event.getData()));
|
switch (event.getEvent()) {
|
||||||
|
case SSE_EVENT_TYPE_ACTIONS:
|
||||||
|
// We could use the actions payload here directly BUT as of March 2022 there is a bug in the cloud
|
||||||
|
// that makes the payload differ from the actual values. The /actions endpoint delivers the correct
|
||||||
|
// data. Thus, receiving an actions update via SSE is used as a trigger to fetch the actions state
|
||||||
|
// from the /actions endpoint as a workaround. See
|
||||||
|
// https://github.com/openhab/openhab-addons/issues/12500
|
||||||
|
for (String deviceIdentifier : ActionsCollection.fromJson(event.getData()).getDeviceIdentifiers()) {
|
||||||
|
try {
|
||||||
|
fetchActions(deviceIdentifier);
|
||||||
|
} catch (MieleWebserviceException e) {
|
||||||
|
logger.warn("Failed to fetch action state for device {}: {} - {}", deviceIdentifier,
|
||||||
|
e.getConnectionError(), e.getMessage());
|
||||||
|
} catch (AuthorizationFailedException e) {
|
||||||
|
logger.warn("Failed to fetch action state for device {}: {}", deviceIdentifier,
|
||||||
|
e.getMessage());
|
||||||
|
onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
|
||||||
|
break;
|
||||||
|
} catch (TooManyRequestsException e) {
|
||||||
|
logger.warn("Failed to fetch action state for device {}: {}", deviceIdentifier,
|
||||||
|
e.getMessage());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SSE_EVENT_TYPE_DEVICES:
|
||||||
|
deviceStateDispatcher.dispatchDeviceStateUpdates(DeviceCollection.fromJson(event.getData()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
} catch (MieleSyntaxException e) {
|
} catch (MieleSyntaxException e) {
|
||||||
logger.warn("SSE payload is not valid Json: {}", event.getData());
|
logger.warn("SSE payload is not valid Json: {}", event.getData());
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2022 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.mielecloud.internal.webservice.api.json;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable POJO representing a collection of actions queried from the Miele REST API.
|
||||||
|
*
|
||||||
|
* @author Björn Lange - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ActionsCollection {
|
||||||
|
private static final java.lang.reflect.Type STRING_ACTIONS_MAP_TYPE = new TypeToken<Map<String, Actions>>() {
|
||||||
|
}.getType();
|
||||||
|
|
||||||
|
private final Map<String, Actions> actions;
|
||||||
|
|
||||||
|
ActionsCollection(Map<String, Actions> actions) {
|
||||||
|
this.actions = actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link ActionsCollection} from the given Json text.
|
||||||
|
*
|
||||||
|
* @param json The Json text.
|
||||||
|
* @return The created {@link ActionsCollection}.
|
||||||
|
* @throws MieleSyntaxException if parsing the data from {@code json} fails.
|
||||||
|
*/
|
||||||
|
public static ActionsCollection fromJson(String json) {
|
||||||
|
try {
|
||||||
|
Map<String, Actions> actions = new Gson().fromJson(json, STRING_ACTIONS_MAP_TYPE);
|
||||||
|
if (actions == null) {
|
||||||
|
throw new MieleSyntaxException("Failed to parse Json.");
|
||||||
|
}
|
||||||
|
return new ActionsCollection(actions);
|
||||||
|
} catch (JsonSyntaxException e) {
|
||||||
|
throw new MieleSyntaxException("Failed to parse Json.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getDeviceIdentifiers() {
|
||||||
|
return actions.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Actions getActions(String identifier) {
|
||||||
|
Actions actions = this.actions.get(identifier);
|
||||||
|
if (actions == null) {
|
||||||
|
throw new IllegalArgumentException("There are no actions for identifier " + identifier);
|
||||||
|
}
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ActionsCollection other = (ActionsCollection) obj;
|
||||||
|
return Objects.equals(actions, other.actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ActionsCollection [actions=" + actions + "]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ public final class ServerSentEvent {
|
||||||
private final String event;
|
private final String event;
|
||||||
private final String data;
|
private final String data;
|
||||||
|
|
||||||
ServerSentEvent(String event, String data) {
|
public ServerSentEvent(String event, String data) {
|
||||||
this.event = event;
|
this.event = event;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,178 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) 2010-2022 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.mielecloud.internal.webservice;
|
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.ScheduledFuture;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.mockito.ArgumentMatchers;
|
|
||||||
import org.mockito.invocation.InvocationOnMock;
|
|
||||||
import org.mockito.stubbing.Answer;
|
|
||||||
import org.openhab.binding.mielecloud.internal.util.MockUtil;
|
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.api.DeviceState;
|
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.api.json.StateType;
|
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.exception.AuthorizationFailedException;
|
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.exception.MieleWebserviceException;
|
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.exception.TooManyRequestsException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Björn Lange - Initial Contribution
|
|
||||||
*/
|
|
||||||
@NonNullByDefault
|
|
||||||
public class ActionStateFetcherTest {
|
|
||||||
private ScheduledExecutorService mockImmediatelyExecutingExecutorService() {
|
|
||||||
ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class);
|
|
||||||
when(scheduler.submit(ArgumentMatchers.<Runnable> any()))
|
|
||||||
.thenAnswer(new Answer<@Nullable ScheduledFuture<?>>() {
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
public ScheduledFuture<?> answer(@Nullable InvocationOnMock invocation) throws Throwable {
|
|
||||||
((Runnable) MockUtil.requireNonNull(invocation).getArgument(0)).run();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return scheduler;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testFetchActionsIsInvokedWhenInitialDeviceStateIsSet() {
|
|
||||||
// given:
|
|
||||||
ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
|
|
||||||
|
|
||||||
MieleWebservice webservice = mock(MieleWebservice.class);
|
|
||||||
DeviceState deviceState = mock(DeviceState.class);
|
|
||||||
DeviceState newDeviceState = mock(DeviceState.class);
|
|
||||||
ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
|
|
||||||
|
|
||||||
when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
|
|
||||||
when(newDeviceState.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
|
|
||||||
|
|
||||||
// when:
|
|
||||||
actionsfetcher.onDeviceStateUpdated(deviceState);
|
|
||||||
|
|
||||||
// then:
|
|
||||||
verify(webservice).fetchActions(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testFetchActionsIsInvokedOnStateTransition() {
|
|
||||||
// given:
|
|
||||||
ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
|
|
||||||
|
|
||||||
MieleWebservice webservice = mock(MieleWebservice.class);
|
|
||||||
DeviceState deviceState = mock(DeviceState.class);
|
|
||||||
DeviceState newDeviceState = mock(DeviceState.class);
|
|
||||||
ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
|
|
||||||
|
|
||||||
when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
|
|
||||||
when(newDeviceState.getStateType()).thenReturn(Optional.of(StateType.END_PROGRAMMED));
|
|
||||||
|
|
||||||
actionsfetcher.onDeviceStateUpdated(deviceState);
|
|
||||||
|
|
||||||
// when:
|
|
||||||
actionsfetcher.onDeviceStateUpdated(newDeviceState);
|
|
||||||
|
|
||||||
// then:
|
|
||||||
verify(webservice, times(2)).fetchActions(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testFetchActionsIsNotInvokedWhenNoStateTransitionOccurrs() {
|
|
||||||
// given:
|
|
||||||
ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
|
|
||||||
|
|
||||||
MieleWebservice webservice = mock(MieleWebservice.class);
|
|
||||||
DeviceState deviceState = mock(DeviceState.class);
|
|
||||||
DeviceState newDeviceState = mock(DeviceState.class);
|
|
||||||
ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
|
|
||||||
|
|
||||||
when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
|
|
||||||
when(newDeviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
|
|
||||||
|
|
||||||
actionsfetcher.onDeviceStateUpdated(deviceState);
|
|
||||||
|
|
||||||
// when:
|
|
||||||
actionsfetcher.onDeviceStateUpdated(newDeviceState);
|
|
||||||
|
|
||||||
// then:
|
|
||||||
verify(webservice, times(1)).fetchActions(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenFetchActionsFailsWithAMieleWebserviceExceptionThenNoExceptionIsThrown() {
|
|
||||||
// given:
|
|
||||||
ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
|
|
||||||
|
|
||||||
MieleWebservice webservice = mock(MieleWebservice.class);
|
|
||||||
doThrow(new MieleWebserviceException("It went wrong", ConnectionError.REQUEST_EXECUTION_FAILED))
|
|
||||||
.when(webservice).fetchActions(any());
|
|
||||||
|
|
||||||
DeviceState deviceState = mock(DeviceState.class);
|
|
||||||
when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
|
|
||||||
|
|
||||||
ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
|
|
||||||
|
|
||||||
// when:
|
|
||||||
actionsfetcher.onDeviceStateUpdated(deviceState);
|
|
||||||
|
|
||||||
// then:
|
|
||||||
verify(webservice, times(1)).fetchActions(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenFetchActionsFailsWithAnAuthorizationFailedExceptionThenNoExceptionIsThrown() {
|
|
||||||
// given:
|
|
||||||
ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
|
|
||||||
|
|
||||||
MieleWebservice webservice = mock(MieleWebservice.class);
|
|
||||||
doThrow(new AuthorizationFailedException("Authorization failed")).when(webservice).fetchActions(any());
|
|
||||||
|
|
||||||
DeviceState deviceState = mock(DeviceState.class);
|
|
||||||
when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
|
|
||||||
|
|
||||||
ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
|
|
||||||
|
|
||||||
// when:
|
|
||||||
actionsfetcher.onDeviceStateUpdated(deviceState);
|
|
||||||
|
|
||||||
// then:
|
|
||||||
verify(webservice, times(1)).fetchActions(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void whenFetchActionsFailsWithATooManyRequestsExceptionThenNoExceptionIsThrown() {
|
|
||||||
// given:
|
|
||||||
ScheduledExecutorService scheduler = mockImmediatelyExecutingExecutorService();
|
|
||||||
|
|
||||||
MieleWebservice webservice = mock(MieleWebservice.class);
|
|
||||||
doThrow(new TooManyRequestsException("Too many requests", null)).when(webservice).fetchActions(any());
|
|
||||||
|
|
||||||
DeviceState deviceState = mock(DeviceState.class);
|
|
||||||
when(deviceState.getStateType()).thenReturn(Optional.of(StateType.RUNNING));
|
|
||||||
|
|
||||||
ActionStateFetcher actionsfetcher = new ActionStateFetcher(() -> webservice, scheduler);
|
|
||||||
|
|
||||||
// when:
|
|
||||||
actionsfetcher.onDeviceStateUpdated(deviceState);
|
|
||||||
|
|
||||||
// then:
|
|
||||||
verify(webservice, times(1)).fetchActions(any());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -43,6 +43,7 @@ import org.openhab.binding.mielecloud.internal.webservice.retry.AuthorizationFai
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.retry.NTimesRetryStrategy;
|
import org.openhab.binding.mielecloud.internal.webservice.retry.NTimesRetryStrategy;
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategy;
|
import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategy;
|
||||||
import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategyCombiner;
|
import org.openhab.binding.mielecloud.internal.webservice.retry.RetryStrategyCombiner;
|
||||||
|
import org.openhab.binding.mielecloud.internal.webservice.sse.ServerSentEvent;
|
||||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -58,7 +59,8 @@ public class DefaultMieleWebserviceTest {
|
||||||
|
|
||||||
private static final String SERVER_ADDRESS = "https://api.mcs3.miele.com";
|
private static final String SERVER_ADDRESS = "https://api.mcs3.miele.com";
|
||||||
private static final String ENDPOINT_DEVICES = SERVER_ADDRESS + "/v1/devices/";
|
private static final String ENDPOINT_DEVICES = SERVER_ADDRESS + "/v1/devices/";
|
||||||
private static final String ENDPOINT_ACTIONS = ENDPOINT_DEVICES + DEVICE_IDENTIFIER + "/actions";
|
private static final String ENDPOINT_EXTENSION_ACTIONS = "/actions";
|
||||||
|
private static final String ENDPOINT_ACTIONS = ENDPOINT_DEVICES + DEVICE_IDENTIFIER + ENDPOINT_EXTENSION_ACTIONS;
|
||||||
private static final String ENDPOINT_LOGOUT = SERVER_ADDRESS + "/thirdparty/logout";
|
private static final String ENDPOINT_LOGOUT = SERVER_ADDRESS + "/thirdparty/logout";
|
||||||
|
|
||||||
private static final String ACCESS_TOKEN = "DE_0123456789abcdef0123456789abcdef";
|
private static final String ACCESS_TOKEN = "DE_0123456789abcdef0123456789abcdef";
|
||||||
|
@ -721,6 +723,214 @@ public class DefaultMieleWebserviceTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void receivingSseActionsEventNotifiesConnectionAlive() throws Exception {
|
||||||
|
// given:
|
||||||
|
var requestFactory = mock(RequestFactory.class);
|
||||||
|
var dispatcher = mock(DeviceStateDispatcher.class);
|
||||||
|
var scheduler = mock(ScheduledExecutorService.class);
|
||||||
|
|
||||||
|
var connectionStatusListener = mock(ConnectionStatusListener.class);
|
||||||
|
|
||||||
|
try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
|
||||||
|
scheduler)) {
|
||||||
|
webservice.addConnectionStatusListener(connectionStatusListener);
|
||||||
|
|
||||||
|
var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS, "{}");
|
||||||
|
|
||||||
|
// when:
|
||||||
|
webservice.onServerSentEvent(actionsEvent);
|
||||||
|
|
||||||
|
// then:
|
||||||
|
verify(connectionStatusListener).onConnectionAlive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void receivingSseActionsEventWithNonJsonPayloadDoesNothing() throws Exception {
|
||||||
|
// given:
|
||||||
|
var requestFactory = mock(RequestFactory.class);
|
||||||
|
var dispatcher = mock(DeviceStateDispatcher.class);
|
||||||
|
var scheduler = mock(ScheduledExecutorService.class);
|
||||||
|
|
||||||
|
try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
|
||||||
|
scheduler)) {
|
||||||
|
webservice.setAccessToken(ACCESS_TOKEN);
|
||||||
|
|
||||||
|
var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
|
||||||
|
"{\"" + DEVICE_IDENTIFIER + "\": {}");
|
||||||
|
|
||||||
|
// when:
|
||||||
|
webservice.onServerSentEvent(actionsEvent);
|
||||||
|
|
||||||
|
// then:
|
||||||
|
verifyNoMoreInteractions(dispatcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void receivingSseActionsEventFetchesActionsForADevice() throws Exception {
|
||||||
|
// given:
|
||||||
|
var requestFactory = mock(RequestFactory.class);
|
||||||
|
when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
|
||||||
|
|
||||||
|
var response = createContentResponseMock(200, "{}");
|
||||||
|
when(request.send()).thenReturn(response);
|
||||||
|
|
||||||
|
var dispatcher = mock(DeviceStateDispatcher.class);
|
||||||
|
var scheduler = mock(ScheduledExecutorService.class);
|
||||||
|
|
||||||
|
try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
|
||||||
|
scheduler)) {
|
||||||
|
webservice.setAccessToken(ACCESS_TOKEN);
|
||||||
|
|
||||||
|
var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
|
||||||
|
"{\"" + DEVICE_IDENTIFIER + "\": {}}");
|
||||||
|
|
||||||
|
// when:
|
||||||
|
webservice.onServerSentEvent(actionsEvent);
|
||||||
|
|
||||||
|
// then:
|
||||||
|
verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
|
||||||
|
verifyNoMoreInteractions(dispatcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void receivingSseActionsEventFetchesActionsForMultipleDevices() throws Exception {
|
||||||
|
// given:
|
||||||
|
var otherRequest = mock(Request.class);
|
||||||
|
var otherDeviceIdentifier = "000124430017";
|
||||||
|
|
||||||
|
var requestFactory = mock(RequestFactory.class);
|
||||||
|
when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
|
||||||
|
when(requestFactory.createGetRequest(ENDPOINT_DEVICES + otherDeviceIdentifier + ENDPOINT_EXTENSION_ACTIONS,
|
||||||
|
ACCESS_TOKEN)).thenReturn(otherRequest);
|
||||||
|
|
||||||
|
var response = createContentResponseMock(200, "{}");
|
||||||
|
when(request.send()).thenReturn(response);
|
||||||
|
when(otherRequest.send()).thenReturn(response);
|
||||||
|
|
||||||
|
var dispatcher = mock(DeviceStateDispatcher.class);
|
||||||
|
var scheduler = mock(ScheduledExecutorService.class);
|
||||||
|
|
||||||
|
try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
|
||||||
|
scheduler)) {
|
||||||
|
webservice.setAccessToken(ACCESS_TOKEN);
|
||||||
|
|
||||||
|
var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
|
||||||
|
"{\"" + DEVICE_IDENTIFIER + "\": {}, \"" + otherDeviceIdentifier + "\": {}}");
|
||||||
|
|
||||||
|
// when:
|
||||||
|
webservice.onServerSentEvent(actionsEvent);
|
||||||
|
|
||||||
|
// then:
|
||||||
|
verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
|
||||||
|
verify(dispatcher).dispatchActionStateUpdates(eq(otherDeviceIdentifier), any());
|
||||||
|
verifyNoMoreInteractions(dispatcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenFetchingActionsAfterReceivingSseActionsEventFailsForADeviceThenNothingHappensForThisDevice()
|
||||||
|
throws Exception {
|
||||||
|
// given:
|
||||||
|
var otherRequest = mock(Request.class);
|
||||||
|
var otherDeviceIdentifier = "000124430017";
|
||||||
|
|
||||||
|
var requestFactory = mock(RequestFactory.class);
|
||||||
|
when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
|
||||||
|
when(requestFactory.createGetRequest(ENDPOINT_DEVICES + otherDeviceIdentifier + ENDPOINT_EXTENSION_ACTIONS,
|
||||||
|
ACCESS_TOKEN)).thenReturn(otherRequest);
|
||||||
|
|
||||||
|
var response = createContentResponseMock(200, "{}");
|
||||||
|
when(request.send()).thenReturn(response);
|
||||||
|
var otherResponse = createContentResponseMock(405, "{\"message\": \"HTTP 405 Method Not Allowed\"}");
|
||||||
|
when(otherRequest.send()).thenReturn(otherResponse);
|
||||||
|
|
||||||
|
var dispatcher = mock(DeviceStateDispatcher.class);
|
||||||
|
var scheduler = mock(ScheduledExecutorService.class);
|
||||||
|
|
||||||
|
try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
|
||||||
|
scheduler)) {
|
||||||
|
webservice.setAccessToken(ACCESS_TOKEN);
|
||||||
|
|
||||||
|
var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
|
||||||
|
"{\"" + DEVICE_IDENTIFIER + "\": {}, \"" + otherDeviceIdentifier + "\": {}}");
|
||||||
|
|
||||||
|
// when:
|
||||||
|
webservice.onServerSentEvent(actionsEvent);
|
||||||
|
|
||||||
|
// then:
|
||||||
|
verify(dispatcher).dispatchActionStateUpdates(eq(DEVICE_IDENTIFIER), any());
|
||||||
|
verifyNoMoreInteractions(dispatcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenFetchingActionsAfterReceivingSseActionsEventFailsBecauseOfTooManyRequestsThenNothingHappens()
|
||||||
|
throws Exception {
|
||||||
|
// given:
|
||||||
|
var requestFactory = mock(RequestFactory.class);
|
||||||
|
when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
|
||||||
|
|
||||||
|
var response = createContentResponseMock(429, "{\"message\": \"Too Many Requests\"}");
|
||||||
|
when(request.send()).thenReturn(response);
|
||||||
|
|
||||||
|
var headerFields = mock(HttpFields.class);
|
||||||
|
when(headerFields.containsKey(anyString())).thenReturn(false);
|
||||||
|
when(response.getHeaders()).thenReturn(headerFields);
|
||||||
|
|
||||||
|
var dispatcher = mock(DeviceStateDispatcher.class);
|
||||||
|
var scheduler = mock(ScheduledExecutorService.class);
|
||||||
|
|
||||||
|
try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
|
||||||
|
scheduler)) {
|
||||||
|
webservice.setAccessToken(ACCESS_TOKEN);
|
||||||
|
|
||||||
|
var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
|
||||||
|
"{\"" + DEVICE_IDENTIFIER + "\": {}}");
|
||||||
|
|
||||||
|
// when:
|
||||||
|
webservice.onServerSentEvent(actionsEvent);
|
||||||
|
|
||||||
|
// then:
|
||||||
|
verifyNoMoreInteractions(dispatcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void whenFetchingActionsAfterReceivingSseActionsEventFailsBecauseOfAuthorizationFailedThenThisIsNotifiedToListeners()
|
||||||
|
throws Exception {
|
||||||
|
// given:
|
||||||
|
var requestFactory = mock(RequestFactory.class);
|
||||||
|
when(requestFactory.createGetRequest(ENDPOINT_ACTIONS, ACCESS_TOKEN)).thenReturn(request);
|
||||||
|
|
||||||
|
var response = createContentResponseMock(401, "{\"message\": \"Unauthorized\"}");
|
||||||
|
when(request.send()).thenReturn(response);
|
||||||
|
|
||||||
|
var dispatcher = mock(DeviceStateDispatcher.class);
|
||||||
|
var scheduler = mock(ScheduledExecutorService.class);
|
||||||
|
|
||||||
|
var connectionStatusListener = mock(ConnectionStatusListener.class);
|
||||||
|
|
||||||
|
try (var webservice = new DefaultMieleWebservice(requestFactory, new NTimesRetryStrategy(0), dispatcher,
|
||||||
|
scheduler)) {
|
||||||
|
webservice.addConnectionStatusListener(connectionStatusListener);
|
||||||
|
webservice.setAccessToken(ACCESS_TOKEN);
|
||||||
|
|
||||||
|
var actionsEvent = new ServerSentEvent(DefaultMieleWebservice.SSE_EVENT_TYPE_ACTIONS,
|
||||||
|
"{\"" + DEVICE_IDENTIFIER + "\": {}}");
|
||||||
|
|
||||||
|
// when:
|
||||||
|
webservice.onServerSentEvent(actionsEvent);
|
||||||
|
|
||||||
|
// then:
|
||||||
|
verifyNoMoreInteractions(dispatcher);
|
||||||
|
verify(connectionStatusListener).onConnectionError(ConnectionError.AUTHORIZATION_FAILED, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link RetryStrategy} for testing purposes. No exceptions will be catched.
|
* {@link RetryStrategy} for testing purposes. No exceptions will be catched.
|
||||||
*
|
*
|
||||||
|
|
|
@ -18,6 +18,7 @@ import static org.mockito.Mockito.*;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -128,9 +129,11 @@ public class ActionsStateTest {
|
||||||
|
|
||||||
// when:
|
// when:
|
||||||
boolean canBeStarted = actionState.canBeStarted();
|
boolean canBeStarted = actionState.canBeStarted();
|
||||||
|
boolean canBeStopped = actionState.canBeStopped();
|
||||||
|
|
||||||
// then:
|
// then:
|
||||||
assertTrue(canBeStarted);
|
assertTrue(canBeStarted);
|
||||||
|
assertFalse(canBeStopped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -141,9 +144,27 @@ public class ActionsStateTest {
|
||||||
when(actions.getProcessAction()).thenReturn(Collections.singletonList(ProcessAction.STOP));
|
when(actions.getProcessAction()).thenReturn(Collections.singletonList(ProcessAction.STOP));
|
||||||
|
|
||||||
// when:
|
// when:
|
||||||
|
boolean canBeStarted = actionState.canBeStarted();
|
||||||
boolean canBeStopped = actionState.canBeStopped();
|
boolean canBeStopped = actionState.canBeStopped();
|
||||||
|
|
||||||
// then:
|
// then:
|
||||||
|
assertFalse(canBeStarted);
|
||||||
|
assertTrue(canBeStopped);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReturnValueWhenProcessActionStartAndStopAreAvailable() {
|
||||||
|
// given:
|
||||||
|
Actions actions = mock(Actions.class);
|
||||||
|
ActionsState actionState = new ActionsState(DEVICE_IDENTIFIER, actions);
|
||||||
|
when(actions.getProcessAction()).thenReturn(List.of(ProcessAction.START, ProcessAction.STOP));
|
||||||
|
|
||||||
|
// when:
|
||||||
|
boolean canBeStarted = actionState.canBeStarted();
|
||||||
|
boolean canBeStopped = actionState.canBeStopped();
|
||||||
|
|
||||||
|
// then:
|
||||||
|
assertTrue(canBeStarted);
|
||||||
assertTrue(canBeStopped);
|
assertTrue(canBeStopped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2022 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.mielecloud.internal.webservice.api.json;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.openhab.binding.mielecloud.internal.util.ResourceUtil.getResourceAsString;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Björn Lange - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ActionsCollectionTest {
|
||||||
|
@Test
|
||||||
|
public void canCreateActionsCollection() throws IOException {
|
||||||
|
// given:
|
||||||
|
String json = getResourceAsString(
|
||||||
|
"/org/openhab/binding/mielecloud/internal/webservice/api/json/actionsCollection.json");
|
||||||
|
|
||||||
|
// when:
|
||||||
|
ActionsCollection collection = ActionsCollection.fromJson(json);
|
||||||
|
|
||||||
|
// then:
|
||||||
|
assertEquals(Collections.singleton("000123456789"), collection.getDeviceIdentifiers());
|
||||||
|
Actions actions = collection.getActions("000123456789");
|
||||||
|
|
||||||
|
assertEquals(List.of(ProcessAction.START, ProcessAction.STOP), actions.getProcessAction());
|
||||||
|
assertEquals(Collections.singletonList(Light.DISABLE), actions.getLight());
|
||||||
|
assertEquals(Optional.empty(), actions.getStartTime());
|
||||||
|
assertEquals(Collections.singletonList(123), actions.getProgramId());
|
||||||
|
assertEquals(Optional.of(true), actions.getPowerOn());
|
||||||
|
assertEquals(Optional.of(false), actions.getPowerOff());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void creatingActionsCollectionFromInvalidJsonThrowsMieleSyntaxException() {
|
||||||
|
// given:
|
||||||
|
String invalidJson = "{\":{}}";
|
||||||
|
|
||||||
|
// when:
|
||||||
|
assertThrows(MieleSyntaxException.class, () -> {
|
||||||
|
ActionsCollection.fromJson(invalidJson);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void canCreateActionsCollectionWithLargeProgramID() throws IOException {
|
||||||
|
// given:
|
||||||
|
String json = "{\"mac-00124B000AE539D6\": {}}";
|
||||||
|
|
||||||
|
// when:
|
||||||
|
DeviceCollection collection = DeviceCollection.fromJson(json);
|
||||||
|
|
||||||
|
// then:
|
||||||
|
assertEquals(Collections.singleton("mac-00124B000AE539D6"), collection.getDeviceIdentifiers());
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,8 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -100,4 +102,28 @@ public class ActionsTest {
|
||||||
// then:
|
// then:
|
||||||
assertEquals(Arrays.asList(1, 2, 3, 4), actions.getProgramId());
|
assertEquals(Arrays.asList(1, 2, 3, 4), actions.getProgramId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void processActionContainsSingleEntryWhenThereIsOneProcessAction() {
|
||||||
|
// given:
|
||||||
|
String json = "{ \"processAction\": [1] }";
|
||||||
|
|
||||||
|
// when:
|
||||||
|
Actions actions = new Gson().fromJson(json, Actions.class);
|
||||||
|
|
||||||
|
// then:
|
||||||
|
assertEquals(Collections.singletonList(ProcessAction.START), actions.getProcessAction());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void processActionContainsTwoEntriesWhenThereAreTwoProcessActions() {
|
||||||
|
// given:
|
||||||
|
String json = "{ \"processAction\": [1,2] }";
|
||||||
|
|
||||||
|
// when:
|
||||||
|
Actions actions = new Gson().fromJson(json, Actions.class);
|
||||||
|
|
||||||
|
// then:
|
||||||
|
assertEquals(List.of(ProcessAction.START, ProcessAction.STOP), actions.getProcessAction());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"000123456789": {
|
||||||
|
"processAction": [1, 2],
|
||||||
|
"light": [2],
|
||||||
|
"ambientLight": [],
|
||||||
|
"startTime": null,
|
||||||
|
"ventilationStep": [],
|
||||||
|
"programId": [123],
|
||||||
|
"targetTemperature": [],
|
||||||
|
"deviceName": false,
|
||||||
|
"powerOn": true,
|
||||||
|
"powerOff": false,
|
||||||
|
"colors": [],
|
||||||
|
"modes": []
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue